From 1b069cb95eb38a7a277d7023ca999ef754a5c007 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 19 Mar 2026 22:07:34 +0530 Subject: [PATCH 01/58] fix: add ability to re-write class name references in desugar plugin Signed-off-by: Akash Yadav --- .../ClassRefReplacingMethodVisitor.kt | 131 +++++++ .../desugaring/DesugarClassVisitor.kt | 119 ++++-- .../desugaring/DesugarClassVisitorFactory.kt | 109 +++--- .../androidide/desugaring/DesugarParams.kt | 78 ++-- .../dsl/DesugarReplacementsContainer.kt | 226 +++++------ .../androidide/desugaring/dsl/MethodOpcode.kt | 62 +-- .../desugaring/dsl/ReplaceClassRef.kt | 31 ++ .../desugaring/dsl/ReplaceMethodInsn.kt | 363 +++++++++--------- .../desugaring/dsl/ReplaceMethodInsnKey.kt | 10 +- 9 files changed, 670 insertions(+), 459 deletions(-) create mode 100644 composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/ClassRefReplacingMethodVisitor.kt create mode 100644 composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceClassRef.kt diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/ClassRefReplacingMethodVisitor.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/ClassRefReplacingMethodVisitor.kt new file mode 100644 index 0000000000..6eab4658bb --- /dev/null +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/ClassRefReplacingMethodVisitor.kt @@ -0,0 +1,131 @@ +package com.itsaky.androidide.desugaring + +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Type + +/** + * Replaces all bytecode references to one or more classes within a method body. + * + * Covered visit sites: + * - [visitMethodInsn] — owner and embedded descriptor + * - [visitFieldInsn] — owner and field descriptor + * - [visitTypeInsn] — NEW / CHECKCAST / INSTANCEOF / ANEWARRAY operand + * - [visitLdcInsn] — class-literal Type constants + * - [visitLocalVariable] — local variable descriptor and generic signature + * - [visitMultiANewArrayInsn]— array descriptor + * - [visitTryCatchBlock] — caught exception type + * + * @param classReplacements Mapping from source internal name (slash-notation) + * to target internal name (slash-notation). An empty map is a no-op. + * + * @author Akash Yadav + */ +class ClassRefReplacingMethodVisitor( + api: Int, + mv: MethodVisitor?, + private val classReplacements: Map, +) : MethodVisitor(api, mv) { + + override fun visitMethodInsn( + opcode: Int, + owner: String, + name: String, + descriptor: String, + isInterface: Boolean, + ) { + super.visitMethodInsn( + opcode, + replace(owner), + name, + replaceInDescriptor(descriptor), + isInterface, + ) + } + + override fun visitFieldInsn( + opcode: Int, + owner: String, + name: String, + descriptor: String, + ) { + super.visitFieldInsn( + opcode, + replace(owner), + name, + replaceInDescriptor(descriptor), + ) + } + + override fun visitTypeInsn(opcode: Int, type: String) { + super.visitTypeInsn(opcode, replace(type)) + } + + override fun visitLdcInsn(value: Any?) { + // Replace class-literal constants: Foo.class → Bar.class + if (value is Type && value.sort == Type.OBJECT) { + val replaced = replace(value.internalName) + if (replaced !== value.internalName) { + super.visitLdcInsn(Type.getObjectType(replaced)) + return + } + } + super.visitLdcInsn(value) + } + + override fun visitLocalVariable( + name: String, + descriptor: String, + signature: String?, + start: Label, + end: Label, + index: Int, + ) { + super.visitLocalVariable( + name, + replaceInDescriptor(descriptor), + replaceInSignature(signature), + start, + end, + index, + ) + } + + override fun visitMultiANewArrayInsn(descriptor: String, numDimensions: Int) { + super.visitMultiANewArrayInsn(replaceInDescriptor(descriptor), numDimensions) + } + + override fun visitTryCatchBlock( + start: Label, + end: Label, + handler: Label, + type: String?, + ) { + super.visitTryCatchBlock(start, end, handler, type?.let { replace(it) }) + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** Replaces a bare internal class name (slash-notation). */ + private fun replace(internalName: String): String = + classReplacements[internalName] ?: internalName + + /** + * Substitutes every `L;` token in a JVM descriptor or generic + * signature with `L;`. + */ + private fun replaceInDescriptor(descriptor: String): String { + if (classReplacements.isEmpty()) return descriptor + var result = descriptor + for ((from, to) in classReplacements) { + result = result.replace("L$from;", "L$to;") + } + return result + } + + /** Delegates to [replaceInDescriptor]; returns `null` for `null` input. */ + private fun replaceInSignature(signature: String?): String? = + signature?.let { replaceInDescriptor(it) } +} \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt index 0a0e7b10f2..977aa8e7bc 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt @@ -1,41 +1,106 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - package com.itsaky.androidide.desugaring import com.android.build.api.instrumentation.ClassContext import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor import org.objectweb.asm.MethodVisitor /** * [ClassVisitor] implementation for desugaring. * + * Applies two transformations to every method body, in priority order: + * + * 1. **[DesugarMethodVisitor]** (outermost / highest priority) — fine-grained + * per-method-call replacement defined via [DesugarReplacementsContainer.replaceMethod]. + * Its output flows into the next layer. + * + * 2. **[ClassRefReplacingMethodVisitor]** (innermost) — bulk class-reference + * replacement defined via [DesugarReplacementsContainer.replaceClass]. + * Handles every site where a class name can appear in a method body. + * + * Class references that appear in field and method *declarations* (descriptors + * and generic signatures at the class-structure level) are also rewritten here. + * * @author Akash Yadav */ -class DesugarClassVisitor(private val params: DesugarParams, - private val classContext: ClassContext, api: Int, - classVisitor: ClassVisitor +class DesugarClassVisitor( + private val params: DesugarParams, + private val classContext: ClassContext, + api: Int, + classVisitor: ClassVisitor, ) : ClassVisitor(api, classVisitor) { - override fun visitMethod(access: Int, name: String?, descriptor: String?, - signature: String?, exceptions: Array? - ): MethodVisitor { - return DesugarMethodVisitor(params, classContext, api, - super.visitMethod(access, name, descriptor, signature, exceptions)) - } -} + /** + * Class replacement map in ASM internal (slash) notation. + * Derived lazily from the dot-notation map stored in [params]. + */ + private val slashClassReplacements: Map by lazy { + params.classReplacements.get() + .entries.associate { (from, to) -> + from.replace('.', '/') to to.replace('.', '/') + } + } + + // ------------------------------------------------------------------------- + // Class-structure level: rewrite descriptors in field / method declarations + // ------------------------------------------------------------------------- + + override fun visitField( + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any?, + ): FieldVisitor? = super.visitField( + access, + name, + replaceInDescriptor(descriptor), + replaceInSignature(signature), + value, + ) + + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array?, + ): MethodVisitor { + // Rewrite the method's own descriptor/signature at the class-structure level. + val base = super.visitMethod( + access, + name, + descriptor?.let { replaceInDescriptor(it) }, + replaceInSignature(signature), + exceptions, + ) + + // Layer 1 — class-reference replacement inside the method body. + // Skip instantiation entirely when there are no class replacements. + val withClassRefs: MethodVisitor = when { + slashClassReplacements.isNotEmpty() -> + ClassRefReplacingMethodVisitor(api, base, slashClassReplacements) + else -> base + } + + // Layer 2 — fine-grained method-call replacement. + // Runs first; any instruction it emits flows through withClassRefs. + return DesugarMethodVisitor(params, classContext, api, withClassRefs) + } + + // ------------------------------------------------------------------------- + // Descriptor / signature helpers + // ------------------------------------------------------------------------- + + private fun replaceInDescriptor(descriptor: String): String { + if (slashClassReplacements.isEmpty()) return descriptor + var result = descriptor + for ((from, to) in slashClassReplacements) { + result = result.replace("L$from;", "L$to;") + } + return result + } + private fun replaceInSignature(signature: String?): String? = + signature?.let { replaceInDescriptor(it) } +} \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt index 069a5ca142..98f6bde68d 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt @@ -1,20 +1,3 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - package com.itsaky.androidide.desugaring import com.android.build.api.instrumentation.AsmClassVisitorFactory @@ -28,51 +11,49 @@ import org.slf4j.LoggerFactory * * @author Akash Yadav */ -abstract class DesugarClassVisitorFactory : - AsmClassVisitorFactory { - - companion object { - - private val log = - LoggerFactory.getLogger(DesugarClassVisitorFactory::class.java) - } - - override fun createClassVisitor(classContext: ClassContext, - nextClassVisitor: ClassVisitor - ): ClassVisitor { - val params = parameters.orNull - if (params == null) { - log.warn("Could not find desugaring parameters. Disabling desugaring.") - return nextClassVisitor - } - - return DesugarClassVisitor(params, classContext, - instrumentationContext.apiVersion.get(), nextClassVisitor) - } - - override fun isInstrumentable(classData: ClassData): Boolean { - val params = parameters.orNull - if (params == null) { - log.warn("Could not find desugaring parameters. Disabling desugaring.") - return false - } - - val isEnabled = params.enabled.get().also { isEnabled -> - log.debug("Is desugaring enabled: $isEnabled") - } - - if (!isEnabled) { - return false - } - - val includedPackages = params.includedPackages.get() - if (includedPackages.isNotEmpty()) { - val className = classData.className - if (!includedPackages.any { className.startsWith(it) }) { - return false - } - } - - return true - } +abstract class DesugarClassVisitorFactory : AsmClassVisitorFactory { + + companion object { + private val log = + LoggerFactory.getLogger(DesugarClassVisitorFactory::class.java) + } + + private val desugarParams: DesugarParams? + get() = parameters.orNull ?: run { + log.warn("Could not find desugaring parameters. Disabling desugaring.") + null + } + + override fun createClassVisitor( + classContext: ClassContext, + nextClassVisitor: ClassVisitor, + ): ClassVisitor { + val params = desugarParams ?: return nextClassVisitor + return DesugarClassVisitor( + params = params, + classContext = classContext, + api = instrumentationContext.apiVersion.get(), + classVisitor = nextClassVisitor, + ) + } + + override fun isInstrumentable(classData: ClassData): Boolean { + val params = desugarParams ?: return false + + val isEnabled = params.enabled.get().also { log.debug("Is desugaring enabled: $it") } + if (!isEnabled) return false + + // Class-reference replacement must scan every class — any class may + // contain a reference to the one being replaced, regardless of package. + if (params.classReplacements.get().isNotEmpty()) return true + + val includedPackages = params.includedPackages.get() + if (includedPackages.isNotEmpty()) { + if (!includedPackages.any { classData.className.startsWith(it) }) { + return false + } + } + + return true + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarParams.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarParams.kt index 1e5905b45c..315288e458 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarParams.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarParams.kt @@ -1,20 +1,3 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - package com.itsaky.androidide.desugaring import com.android.build.api.instrumentation.InstrumentationParameters @@ -32,33 +15,36 @@ import org.gradle.api.tasks.Input */ interface DesugarParams : InstrumentationParameters { - /** - * Whether the desugaring is enabled. - */ - @get:Input - val enabled: Property - - /** - * The replacement instructions. - */ - @get:Input - val replacements: MapProperty - - @get:Input - val includedPackages: SetProperty - - companion object { - - /** - * Sets [DesugarParams] properties from [DesugarExtension]. - */ - fun DesugarParams.setFrom(extension: DesugarExtension) { - replacements.convention(emptyMap()) - includedPackages.convention(emptySet()) - - enabled.set(extension.enabled) - replacements.set(extension.replacements.instructions) - includedPackages.set(extension.replacements.includePackages) - } - } + /** Whether desugaring is enabled. */ + @get:Input + val enabled: Property + + /** Fine-grained method-call replacement instructions. */ + @get:Input + val replacements: MapProperty + + /** Packages to scan for method-level replacements (empty = all packages). */ + @get:Input + val includedPackages: SetProperty + + /** + * Class-level replacement map: dot-notation source class → dot-notation + * target class. Any class may be instrumented when this is non-empty. + */ + @get:Input + val classReplacements: MapProperty + + companion object { + + fun DesugarParams.setFrom(extension: DesugarExtension) { + replacements.convention(emptyMap()) + includedPackages.convention(emptySet()) + classReplacements.convention(emptyMap()) + + enabled.set(extension.enabled) + replacements.set(extension.replacements.instructions) + includedPackages.set(extension.replacements.includePackages) + classReplacements.set(extension.replacements.classReplacements) + } + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/DesugarReplacementsContainer.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/DesugarReplacementsContainer.kt index 057fcc1cb9..1ad5f89f32 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/DesugarReplacementsContainer.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/DesugarReplacementsContainer.kt @@ -1,20 +1,3 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - package com.itsaky.androidide.desugaring.dsl import com.itsaky.androidide.desugaring.internal.parsing.InsnLexer @@ -30,101 +13,126 @@ import javax.inject.Inject /** * Defines replacements for desugaring. * + * Two replacement strategies are supported and can be combined freely: + * + * - **Method-level** ([replaceMethod]): replaces a specific method call with + * another, with full control over opcodes and descriptors. + * - **Class-level** ([replaceClass]): rewrites every bytecode reference to a + * given class (owners, descriptors, type instructions, LDC constants, etc.) + * with a replacement class. This is a broader, structural operation. + * + * When both apply to the same instruction, method-level replacement wins + * because it runs first in the visitor chain. + * * @author Akash Yadav */ abstract class DesugarReplacementsContainer @Inject constructor( - private val objects: ObjectFactory + private val objects: ObjectFactory, ) { - internal val includePackages = TreeSet() - - internal val instructions = - mutableMapOf() - - companion object { - - private val PACKAGE_NAME_REGEX = - Regex("""^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*${'$'}""") - } - - /** - * Adds the given packages to the list of packages that will be scanned for - * the desugaring process. By default, the list of packages is empty. An empty - * list will include all packages. - */ - fun includePackage(vararg packages: String) { - for (pck in packages) { - if (!PACKAGE_NAME_REGEX.matches(pck)) { - throw IllegalArgumentException("Invalid package name: $pck") - } - - includePackages.add(pck) - } - } - - /** - * Removes the given packages from the list of included packages. - */ - fun removePackage(vararg packages: String) { - includePackages.removeAll(packages.toSet()) - } - - /** - * Adds an instruction to replace the given method. - */ - fun replaceMethod(configure: Action) { - val instruction = objects.newInstance(ReplaceMethodInsn::class.java) - configure.execute(instruction) - addReplaceInsns(instruction) - } - - /** - * Replace usage of [sourceMethod] with the [targetMethod]. - */ - @JvmOverloads - fun replaceMethod( - sourceMethod: Method, - targetMethod: Method, - configure: Action = Action {} - ) { - val instruction = ReplaceMethodInsn.forMethods(sourceMethod, targetMethod).build() - configure.execute(instruction) - if (instruction.requireOpcode == MethodOpcode.INVOKEVIRTUAL - && instruction.toOpcode == MethodOpcode.INVOKESTATIC - ) { - ReflectionUtils.validateVirtualToStaticReplacement(sourceMethod, targetMethod) - } - addReplaceInsns(instruction) - } - - /** - * Load instructions from the given file. - */ - fun loadFromFile(file: File) { - val lexer = InsnLexer(file.readText()) - val parser = InsnParser(lexer) - val insns = parser.parse() - addReplaceInsns(insns) - } - - private fun addReplaceInsns(vararg insns: ReplaceMethodInsn - ) { - addReplaceInsns(insns.asIterable()) - } - - private fun addReplaceInsns(insns: Iterable - ) { - for (insn in insns) { - val className = insn.fromClass.replace('/', '.') - val methodName = insn.methodName - val methodDescriptor = insn.methodDescriptor - - insn.requireOpcode ?: run { - insn.requireOpcode = MethodOpcode.ANY - } - - val key = ReplaceMethodInsnKey(className, methodName, methodDescriptor) - this.instructions[key] = insn - } - } + internal val includePackages = TreeSet() + + internal val instructions = + mutableMapOf() + + /** Class-level replacements: dot-notation source → dot-notation target. */ + internal val classReplacements = mutableMapOf() + + companion object { + private val PACKAGE_NAME_REGEX = + Regex("""^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*${'$'}""") + } + + fun includePackage(vararg packages: String) { + for (pck in packages) { + if (!PACKAGE_NAME_REGEX.matches(pck)) { + throw IllegalArgumentException("Invalid package name: $pck") + } + includePackages.add(pck) + } + } + + fun removePackage(vararg packages: String) { + includePackages.removeAll(packages.toSet()) + } + + fun replaceMethod(configure: Action) { + val instruction = objects.newInstance(ReplaceMethodInsn::class.java) + configure.execute(instruction) + addReplaceInsns(instruction) + } + + @JvmOverloads + fun replaceMethod( + sourceMethod: Method, + targetMethod: Method, + configure: Action = Action {}, + ) { + val instruction = ReplaceMethodInsn.forMethods(sourceMethod, targetMethod).build() + configure.execute(instruction) + if (instruction.requireOpcode == MethodOpcode.INVOKEVIRTUAL + && instruction.toOpcode == MethodOpcode.INVOKESTATIC + ) { + ReflectionUtils.validateVirtualToStaticReplacement(sourceMethod, targetMethod) + } + addReplaceInsns(instruction) + } + + /** + * Replaces every bytecode reference to [fromClass] with [toClass]. + * + * This rewrites: + * - Instruction owners (`INVOKEVIRTUAL`, `GETFIELD`, `NEW`, `CHECKCAST`, …) + * - Type descriptors and generic signatures in method bodies + * - Class-literal LDC constants (`Foo.class`) + * - Field and method *declaration* descriptors in the instrumented class + * + * Class names can be provided in dot-notation (`com.example.Foo`) or + * slash-notation (`com/example/Foo`). + * + * Note: unlike [replaceMethod], class-level replacement is applied to + * **all** instrumented classes regardless of [includePackage] filters, + * because any class may contain a reference to the replaced one. + */ + fun replaceClass(fromClass: String, toClass: String) { + require(fromClass.isNotBlank()) { "fromClass must not be blank." } + require(toClass.isNotBlank()) { "toClass must not be blank." } + val from = fromClass.replace('/', '.') + val to = toClass.replace('/', '.') + classReplacements[from] = to + } + + /** + * Replaces every bytecode reference to [fromClass] with [toClass]. + * + * @throws UnsupportedOperationException for array or primitive types. + */ + fun replaceClass(fromClass: Class<*>, toClass: Class<*>) { + require(!fromClass.isArray && !fromClass.isPrimitive) { + "Array and primitive types are not supported for class replacement." + } + require(!toClass.isArray && !toClass.isPrimitive) { + "Array and primitive types are not supported for class replacement." + } + replaceClass(fromClass.name, toClass.name) + } + + fun loadFromFile(file: File) { + val lexer = InsnLexer(file.readText()) + val parser = InsnParser(lexer) + val insns = parser.parse() + addReplaceInsns(insns) + } + + private fun addReplaceInsns(vararg insns: ReplaceMethodInsn) = + addReplaceInsns(insns.asIterable()) + + private fun addReplaceInsns(insns: Iterable) { + for (insn in insns) { + val className = insn.fromClass.replace('/', '.') + insn.requireOpcode = insn.requireOpcode ?: MethodOpcode.ANY + val key = ReplaceMethodInsnKey(className, insn.methodName, insn.methodDescriptor) + instructions[key] = insn + } + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/MethodOpcode.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/MethodOpcode.kt index 8317865c2d..bed859bc6d 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/MethodOpcode.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/MethodOpcode.kt @@ -24,42 +24,44 @@ import org.objectweb.asm.Opcodes * * @author Akash Yadav */ -enum class MethodOpcode(val insnName: String, val opcode: Int +enum class MethodOpcode( + val insnName: String, + val opcode: Int, ) { - /** - * The opcode for `invokestatic`. - */ - INVOKESTATIC("invoke-static", Opcodes.INVOKESTATIC), + /** + * The opcode for `invokestatic`. + */ + INVOKESTATIC("invoke-static", Opcodes.INVOKESTATIC), - /** - * The opcode for `invokespecial`. - */ - INVOKESPECIAL("invoke-special", Opcodes.INVOKESPECIAL), + /** + * The opcode for `invokespecial`. + */ + INVOKESPECIAL("invoke-special", Opcodes.INVOKESPECIAL), - /** - * The opcode for `invokevirtual`. - */ - INVOKEVIRTUAL("invoke-virtual", Opcodes.INVOKEVIRTUAL), + /** + * The opcode for `invokevirtual`. + */ + INVOKEVIRTUAL("invoke-virtual", Opcodes.INVOKEVIRTUAL), - /** - * The opcode for `invokeinterface`. - */ - INVOKEINTERFACE("invoke-interface", Opcodes.INVOKEINTERFACE), + /** + * The opcode for `invokeinterface`. + */ + INVOKEINTERFACE("invoke-interface", Opcodes.INVOKEINTERFACE), - /** - * Any opcode. This is for internal use only. - */ - ANY("invoke-any", 0); + /** + * Any opcode. This is for internal use only. + */ + ANY("invoke-any", 0); - companion object { + companion object { - /** - * Finds the [MethodOpcode] with the given instruction name. - */ - @JvmStatic - fun find(insn: String): MethodOpcode? { - return MethodOpcode.values().find { it.insnName == insn } - } - } + /** + * Finds the [MethodOpcode] with the given instruction name. + */ + @JvmStatic + fun find(insn: String): MethodOpcode? { + return MethodOpcode.values().find { it.insnName == insn } + } + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceClassRef.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceClassRef.kt new file mode 100644 index 0000000000..224ab00ebe --- /dev/null +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceClassRef.kt @@ -0,0 +1,31 @@ +package com.itsaky.androidide.desugaring.dsl + +import java.io.Serializable + +/** + * Describes a full class-reference replacement: every bytecode reference to + * [fromClass] in any instrumented class will be rewritten to [toClass]. + * + * Class names may be given in dot-notation (`com.example.Foo`) or + * slash-notation (`com/example/Foo`); both are normalised internally. + * + * @author Akash Yadav + */ +data class ReplaceClassRef( + /** The class whose references should be replaced (dot-notation). */ + val fromClass: String, + /** The class that should replace all [fromClass] references (dot-notation). */ + val toClass: String, +) : Serializable { + + companion object { + @JvmField + val serialVersionUID = 1L + } + + /** ASM internal name (slash-notation) for [fromClass]. */ + val fromInternal: String get() = fromClass.replace('.', '/') + + /** ASM internal name (slash-notation) for [toClass]. */ + val toInternal: String get() = toClass.replace('.', '/') +} \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsn.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsn.kt index 09c113f283..da5e59255c 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsn.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsn.kt @@ -29,182 +29,189 @@ import java.lang.reflect.Modifier */ interface ReplaceMethodInsn { - /** - * The owner class name for the method to be replaced. The class name must be - * in the form of a fully qualified name or in the binary name format. - */ - var fromClass: String - - /** - * The name of the method to be replaced. - */ - var methodName: String - - /** - * The descriptor of the method to be replaced. This is the method signature - * as it appears in the bytecode. - */ - var methodDescriptor: String - - /** - * The opcode for the method to be replaced. If this is specified, then the - * opcode for the invoked method will be checked against this and the invocation - * will only be replaced of the opcode matches. - * - * This is optional. By default, the invocation will always be replaced. - */ - var requireOpcode: MethodOpcode? - - /** - * The owner class name for the method which will replace the [methodName]. - * The class name must be in the form of a fully qualified name or in the - * binary name format. - */ - var toClass: String - - /** - * The name of the method in [toClass] which will replace the [methodName]. - */ - var toMethod: String - - /** - * The descriptor of the method in [toClass] which will replace the [methodName]. - */ - var toMethodDescriptor: String - - /** - * The opcode for invoking [toMethod] in [toClass]. - */ - var toOpcode: MethodOpcode - - class Builder { - - @JvmField - var fromClass: String = "" - - @JvmField - var methodName: String = "" - - @JvmField - var methodDescriptor: String = "" - - @JvmField - var requireOpcode: MethodOpcode? = null - - @JvmField - var toClass: String = "" - - @JvmField - var toMethod: String = "" - - @JvmField - var toMethodDescriptor: String = "" - - @JvmField - var toOpcode: MethodOpcode = MethodOpcode.ANY - - fun fromMethod(method: Method) = apply { - fromClass(method.declaringClass) - methodName(method.name) - methodDescriptor(ReflectionUtils.describe(method)) - - if (Modifier.isStatic(method.modifiers)) { - requireOpcode(MethodOpcode.INVOKESTATIC) - } else { - requireOpcode(MethodOpcode.INVOKEVIRTUAL) - } - } - - fun fromClass(fromClass: String) = apply { - this.fromClass = fromClass - } - - fun fromClass(klass: Class<*>): Builder { - if (klass.isArray || klass.isPrimitive) { - throw UnsupportedOperationException( - "Array and primitive types are not supported for desugaring") - } - - return fromClass(klass.name) - } - - fun methodName(methodName: String) = apply { - this.methodName = methodName - } - - fun methodDescriptor(methodDescriptor: String) = apply { - this.methodDescriptor = methodDescriptor - } - - fun requireOpcode(requireOpcode: MethodOpcode) = apply { - this.requireOpcode = requireOpcode - } - - fun toClass(toClass: String) = apply { - this.toClass = toClass - } - - fun toClass(klass: Class<*>): Builder { - if (klass.isArray || klass.isPrimitive) { - throw UnsupportedOperationException( - "Array and primitive types are not supported for desugaring") - } - - return toClass(klass.name) - } - - fun toMethod(toMethod: String) = apply { - this.toMethod = toMethod - } - - fun toMethodDescriptor(toMethodDescriptor: String) = apply { - this.toMethodDescriptor = toMethodDescriptor - } - - fun toMethod(method: Method) = apply { - toClass(method.declaringClass) - toMethod(method.name) - toMethodDescriptor(ReflectionUtils.describe(method)) - - if (Modifier.isStatic(method.modifiers)) { - toOpcode(MethodOpcode.INVOKESTATIC) - } else { - toOpcode(MethodOpcode.INVOKEVIRTUAL) - } - } - - fun toOpcode(toOpcode: MethodOpcode) = apply { - this.toOpcode = toOpcode - } - - fun build(): DefaultReplaceMethodInsn { - require(fromClass.isNotBlank()) { "fromClass cannot be blank." } - require(methodName.isNotBlank()) { "methodName cannot be blank." } - require( - methodDescriptor.isNotBlank()) { "methodDescriptor cannot be blank." } - require(toClass.isNotBlank()) { "toClass cannot be blank." } - require(toMethod.isNotBlank()) { "toMethod cannot be blank." } - require( - toMethodDescriptor.isNotBlank()) { "toMethodDescriptor cannot be blank." } - require(toOpcode != MethodOpcode.ANY) { "toOpcode cannot be ANY." } - - return DefaultReplaceMethodInsn(fromClass, methodName, methodDescriptor, - requireOpcode, toClass, toMethod, toMethodDescriptor, toOpcode) - } - } - - companion object { - - @JvmStatic - fun builder(): Builder = Builder() - - /** - * Creates a [Builder] for the given source and target method. - */ - @JvmStatic - fun forMethods(fromMethod: Method, toMethod: Method - ): Builder { - return builder().fromMethod(fromMethod).toMethod(toMethod) - } - } + /** + * The owner class name for the method to be replaced. The class name must be + * in the form of a fully qualified name or in the binary name format. + */ + var fromClass: String + + /** + * The name of the method to be replaced. + */ + var methodName: String + + /** + * The descriptor of the method to be replaced. This is the method signature + * as it appears in the bytecode. + */ + var methodDescriptor: String + + /** + * The opcode for the method to be replaced. If this is specified, then the + * opcode for the invoked method will be checked against this and the invocation + * will only be replaced of the opcode matches. + * + * This is optional. By default, the invocation will always be replaced. + */ + var requireOpcode: MethodOpcode? + + /** + * The owner class name for the method which will replace the [methodName]. + * The class name must be in the form of a fully qualified name or in the + * binary name format. + */ + var toClass: String + + /** + * The name of the method in [toClass] which will replace the [methodName]. + */ + var toMethod: String + + /** + * The descriptor of the method in [toClass] which will replace the [methodName]. + */ + var toMethodDescriptor: String + + /** + * The opcode for invoking [toMethod] in [toClass]. + */ + var toOpcode: MethodOpcode + + class Builder { + + @JvmField + var fromClass: String = "" + + @JvmField + var methodName: String = "" + + @JvmField + var methodDescriptor: String = "" + + @JvmField + var requireOpcode: MethodOpcode? = null + + @JvmField + var toClass: String = "" + + @JvmField + var toMethod: String = "" + + @JvmField + var toMethodDescriptor: String = "" + + @JvmField + var toOpcode: MethodOpcode = MethodOpcode.ANY + + fun fromMethod(method: Method) = apply { + fromClass(method.declaringClass) + methodName(method.name) + methodDescriptor(ReflectionUtils.describe(method)) + + if (Modifier.isStatic(method.modifiers)) { + requireOpcode(MethodOpcode.INVOKESTATIC) + } else { + requireOpcode(MethodOpcode.INVOKEVIRTUAL) + } + } + + fun fromClass(fromClass: String) = apply { + this.fromClass = fromClass + } + + fun fromClass(klass: Class<*>): Builder { + if (klass.isArray || klass.isPrimitive) { + throw UnsupportedOperationException( + "Array and primitive types are not supported for desugaring" + ) + } + + return fromClass(klass.name) + } + + fun methodName(methodName: String) = apply { + this.methodName = methodName + } + + fun methodDescriptor(methodDescriptor: String) = apply { + this.methodDescriptor = methodDescriptor + } + + fun requireOpcode(requireOpcode: MethodOpcode) = apply { + this.requireOpcode = requireOpcode + } + + fun toClass(toClass: String) = apply { + this.toClass = toClass + } + + fun toClass(klass: Class<*>): Builder { + if (klass.isArray || klass.isPrimitive) { + throw UnsupportedOperationException( + "Array and primitive types are not supported for desugaring" + ) + } + + return toClass(klass.name) + } + + fun toMethod(toMethod: String) = apply { + this.toMethod = toMethod + } + + fun toMethodDescriptor(toMethodDescriptor: String) = apply { + this.toMethodDescriptor = toMethodDescriptor + } + + fun toMethod(method: Method) = apply { + toClass(method.declaringClass) + toMethod(method.name) + toMethodDescriptor(ReflectionUtils.describe(method)) + + if (Modifier.isStatic(method.modifiers)) { + toOpcode(MethodOpcode.INVOKESTATIC) + } else { + toOpcode(MethodOpcode.INVOKEVIRTUAL) + } + } + + fun toOpcode(toOpcode: MethodOpcode) = apply { + this.toOpcode = toOpcode + } + + fun build(): DefaultReplaceMethodInsn { + require(fromClass.isNotBlank()) { "fromClass cannot be blank." } + require(methodName.isNotBlank()) { "methodName cannot be blank." } + require( + methodDescriptor.isNotBlank() + ) { "methodDescriptor cannot be blank." } + require(toClass.isNotBlank()) { "toClass cannot be blank." } + require(toMethod.isNotBlank()) { "toMethod cannot be blank." } + require( + toMethodDescriptor.isNotBlank() + ) { "toMethodDescriptor cannot be blank." } + require(toOpcode != MethodOpcode.ANY) { "toOpcode cannot be ANY." } + + return DefaultReplaceMethodInsn( + fromClass, methodName, methodDescriptor, + requireOpcode, toClass, toMethod, toMethodDescriptor, toOpcode + ) + } + } + + companion object { + + @JvmStatic + fun builder(): Builder = Builder() + + /** + * Creates a [Builder] for the given source and target method. + */ + @JvmStatic + fun forMethods( + fromMethod: Method, toMethod: Method + ): Builder { + return builder().fromMethod(fromMethod).toMethod(toMethod) + } + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsnKey.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsnKey.kt index b3d41fbbc9..b25b487ce9 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsnKey.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsnKey.kt @@ -25,10 +25,10 @@ import java.io.Serializable * @author Akash Yadav */ data class ReplaceMethodInsnKey( - val className: String, - val methodName: String, - val methodDescriptor: String + val className: String, + val methodName: String, + val methodDescriptor: String ) : Serializable { - @JvmField - val serialVersionUID = 1L + @JvmField + val serialVersionUID = 1L } From a70fd6627a5bd26c3b4c900223fd193a4551f11f Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 19 Mar 2026 22:36:15 +0530 Subject: [PATCH 02/58] feat: integrate Kotlin analysis API Signed-off-by: Akash Yadav --- app/build.gradle.kts | 23 ++++++++- lsp/kotlin/build.gradle.kts | 1 + settings.gradle.kts | 51 ++++++++++--------- subprojects/kotlin-analysis-api/.gitignore | 1 + .../kotlin-analysis-api/build.gradle.kts | 27 ++++++++++ .../kotlin-analysis-api/consumer-rules.pro | 0 .../kotlin-analysis-api/proguard-rules.pro | 21 ++++++++ 7 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 subprojects/kotlin-analysis-api/.gitignore create mode 100644 subprojects/kotlin-analysis-api/build.gradle.kts create mode 100644 subprojects/kotlin-analysis-api/consumer-rules.pro create mode 100644 subprojects/kotlin-analysis-api/proguard-rules.pro diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b99b2e1bbc..071151d551 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -153,14 +153,29 @@ android { packaging { resources { - excludes.add("META-INF/DEPENDENCIES") - excludes.add("META-INF/gradle/incremental.annotation.processors") + excludes += "META-INF/DEPENDENCIES" + excludes += "META-INF/gradle/incremental.annotation.processors" + + pickFirsts += "kotlin/internal/internal.kotlin_builtins" + pickFirsts += "kotlin/reflect/reflect.kotlin_builtins" + pickFirsts += "kotlin/kotlin.kotlin_builtins" + pickFirsts += "kotlin/coroutines/coroutines.kotlin_builtins" + pickFirsts += "kotlin/ranges/ranges.kotlin_builtins" + pickFirsts += "kotlin/concurrent/atomics/atomics.kotlin_builtins" + pickFirsts += "kotlin/collections/collections.kotlin_builtins" + pickFirsts += "kotlin/annotation/annotation.kotlin_builtins" + + pickFirsts += "META-INF/FastDoubleParser-LICENSE" + pickFirsts += "META-INF/thirdparty-LICENSE" + pickFirsts += "META-INF/FastDoubleParser-NOTICE" + pickFirsts += "META-INF/thirdparty-NOTICE" } jniLibs { useLegacyPackaging = false } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -193,6 +208,10 @@ configurations.matching { it.name.contains("AndroidTest") }.configureEach { exclude(group = "com.google.protobuf", module = "protobuf-lite") } +configurations.configureEach { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-android-extensions-runtime") +} + dependencies { debugImplementation(libs.common.leakcanary) diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index 9de97e6c31..dbb8e664bc 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.lsp.models) implementation(projects.eventbusEvents) implementation(projects.shared) + implementation(projects.subprojects.kotlinAnalysisApi) implementation(projects.subprojects.projects) implementation(projects.subprojects.projectModels) diff --git a/settings.gradle.kts b/settings.gradle.kts index 247903307d..dfb9c6f997 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,25 +38,25 @@ dependencyResolutionManagement { val dependencySubstitutions = mapOf( "build-deps" to - arrayOf( - "appintro", - "fuzzysearch", - "google-java-format", - "java-compiler", - "javac", - "javapoet", - "jaxp", - "jdk-compiler", - "jdk-jdeps", - "jdt", - "layoutlib-api", - "treeview", - ), + arrayOf( + "appintro", + "fuzzysearch", + "google-java-format", + "java-compiler", + "javac", + "javapoet", + "jaxp", + "jdk-compiler", + "jdk-jdeps", + "jdt", + "layoutlib-api", + "treeview", + ), "build-deps-common" to - arrayOf( - "constants", - "desugaring-core", - ), + arrayOf( + "constants", + "desugaring-core", + ), ) for ((build, modules) in dependencySubstitutions) { @@ -123,7 +123,7 @@ include( ":eventbus", ":eventbus-android", ":eventbus-events", - ":git-core", + ":git-core", ":gradle-plugin", ":gradle-plugin-config", ":idetooltips", @@ -155,6 +155,7 @@ include( ":subprojects:flashbar", ":subprojects:framework-stubs", ":subprojects:javac-services", + ":subprojects:kotlin-analysis-api", ":subprojects:libjdwp", ":subprojects:projects", ":subprojects:project-models", @@ -185,12 +186,12 @@ include( ":plugin-api", ":plugin-api:plugin-builder", ":plugin-manager", - ":llama-api", - ":llama-impl", - ":cv-image-to-xml", - ":llama-api", - ":llama-impl", - ":compose-preview" + ":llama-api", + ":llama-impl", + ":cv-image-to-xml", + ":llama-api", + ":llama-impl", + ":compose-preview" ) object FDroidConfig { diff --git a/subprojects/kotlin-analysis-api/.gitignore b/subprojects/kotlin-analysis-api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/subprojects/kotlin-analysis-api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/subprojects/kotlin-analysis-api/build.gradle.kts b/subprojects/kotlin-analysis-api/build.gradle.kts new file mode 100644 index 0000000000..4238540dea --- /dev/null +++ b/subprojects/kotlin-analysis-api/build.gradle.kts @@ -0,0 +1,27 @@ +import com.itsaky.androidide.build.config.BuildConfig +import com.itsaky.androidide.plugins.extension.AssetSource + +plugins { + alias(libs.plugins.android.library) + id("com.itsaky.androidide.build.external-assets") +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.kt.analysis" +} + +val ktAndroidRepo = "https://github.com/appdevforall/kotlin-android" +val ktAndroidVersion = "2.3.255" +val ktAndroidTag = "v${ktAndroidVersion}-073dc78" +val ktAndroidJarName = "analysis-api-standalone-embeddable-for-ide-${ktAndroidVersion}-SNAPSHOT.jar" + +externalAssets { + jarDependency("kt-android") { + configuration = "api" + source = + AssetSource.External( + url = uri("$ktAndroidRepo/releases/download/$ktAndroidTag/$ktAndroidJarName"), + sha256Checksum = "56918aee41a9a1f6bb4df11cdd3b78ff7bcaadbfb6f939f1dd4a645dbfe03cdd", + ) + } +} diff --git a/subprojects/kotlin-analysis-api/consumer-rules.pro b/subprojects/kotlin-analysis-api/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/subprojects/kotlin-analysis-api/proguard-rules.pro b/subprojects/kotlin-analysis-api/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/subprojects/kotlin-analysis-api/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file From 43286a629f99665d26dc8b0bd5d105f1b5a1b005 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Mon, 23 Mar 2026 17:12:08 +0530 Subject: [PATCH 03/58] fix: update kotlin-android to latest version fixes duplicate class errors for org.antrl.v4.* classes Signed-off-by: Akash Yadav --- subprojects/kotlin-analysis-api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subprojects/kotlin-analysis-api/build.gradle.kts b/subprojects/kotlin-analysis-api/build.gradle.kts index 4238540dea..6e11cc1ce0 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}-073dc78" +val ktAndroidTag = "v${ktAndroidVersion}-f1ac8b3" 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 = "56918aee41a9a1f6bb4df11cdd3b78ff7bcaadbfb6f939f1dd4a645dbfe03cdd", + sha256Checksum = "8c7cad7e0905a861048cce000c3ef22d9ad05572b4f9a0830e0c0e0060ddd3c9", ) } } From f1fe62f36a7c70be3c1bfdd8024706bdc0a3979e Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 25 Mar 2026 15:36:00 +0530 Subject: [PATCH 04/58] fix: remove UnsafeImpl It is now included in the embeddable JAR (named UnsafeAndroid) with proper relocations. Signed-off-by: Akash Yadav --- .../intellij/util/containers/UnsafeImpl.java | 129 ------------------ 1 file changed, 129 deletions(-) delete mode 100644 app/src/main/java/org/jetbrains/kotlin/com/intellij/util/containers/UnsafeImpl.java diff --git a/app/src/main/java/org/jetbrains/kotlin/com/intellij/util/containers/UnsafeImpl.java b/app/src/main/java/org/jetbrains/kotlin/com/intellij/util/containers/UnsafeImpl.java deleted file mode 100644 index b38c4f2439..0000000000 --- a/app/src/main/java/org/jetbrains/kotlin/com/intellij/util/containers/UnsafeImpl.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.jetbrains.kotlin.com.intellij.util.containers; - -import android.util.Log; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Arrays; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.kotlin.com.intellij.util.ReflectionUtil; -import org.lsposed.hiddenapibypass.HiddenApiBypass; - -@SuppressWarnings("ALL") -public class UnsafeImpl { - - private static final Object unsafe; - - private static final Method putObjectVolatile; - private static final Method getObjectVolatile; - private static final Method compareAndSwapObject; - private static final Method compareAndSwapInt; - private static final Method compareAndSwapLong; - private static final Method getAndAddInt; - private static final Method objectFieldOffset; - private static final Method arrayIndexScale; - private static final Method arrayBaseOffset; - // private static final Method copyMemory; - - private static final String TAG = "UnsafeImpl"; - - static { - try { - unsafe = ReflectionUtil.getUnsafe(); - putObjectVolatile = find("putObjectVolatile", Object.class, long.class, Object.class); - getObjectVolatile = find("getObjectVolatile", Object.class, long.class); - compareAndSwapObject = find("compareAndSwapObject", Object.class, long.class, Object.class, Object.class); - compareAndSwapInt = find("compareAndSwapInt", Object.class, long.class, int.class, int.class); - compareAndSwapLong = find("compareAndSwapLong", Object.class, long.class, long.class, long.class); - getAndAddInt = find("getAndAddInt", Object.class, long.class, int.class); - objectFieldOffset = find("objectFieldOffset", Field.class); - arrayBaseOffset = find("arrayBaseOffset", Class.class); - arrayIndexScale = find("arrayIndexScale", Class.class); - // copyMemory = find("copyMemory", Object.class, long.class, Object.class, long.class, long.class); - } catch (Throwable t) { - throw new Error(t); - } - } - - public static int arrayBaseOffset(Class arrayClass) { - try { - return (int) arrayBaseOffset.invoke(unsafe, arrayClass); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static int arrayIndexScale(Class arrayClass) { - try { - return (int) arrayIndexScale.invoke(unsafe, arrayClass); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static boolean compareAndSwapInt(Object object, long offset, int expected, int value) { - try { - return (boolean) compareAndSwapInt.invoke(unsafe, object, offset, expected, value); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static boolean compareAndSwapLong(@NotNull Object object, long offset, long expected, long value) { - try { - return (boolean) compareAndSwapLong.invoke(unsafe, object, offset, expected, value); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static boolean compareAndSwapObject(Object o, long offset, Object expected, Object x) { - try { - return (boolean) compareAndSwapObject.invoke(unsafe, o, offset, expected, x); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes) { - throw new UnsupportedOperationException("Not supported on Android!"); - } - - public static int getAndAddInt(Object object, long offset, int v) { - try { - return (int) getAndAddInt.invoke(unsafe, object, offset, v); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static Object getObjectVolatile(Object object, long offset) { - try { - return getObjectVolatile.invoke(unsafe, object, offset); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static long objectFieldOffset(Field f) { - try { - return (long) objectFieldOffset.invoke(unsafe, f); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static void putObjectVolatile(Object o, long offset, Object x) { - try { - putObjectVolatile.invoke(unsafe, o, offset, x); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - private static @NotNull Method find(String name, Class... params) throws Exception { - Log.d(TAG, "find: name=" + name + ", params=" + Arrays.toString(params)); - Method m = HiddenApiBypass.getDeclaredMethod(unsafe.getClass(), name, params); - m.setAccessible(true); - return m; - } -} From 6e6d8b3eeeb77765f75207962695192fee4abbca Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 25 Mar 2026 15:42:11 +0530 Subject: [PATCH 05/58] fix: update kotlin-android to latest version Signed-off-by: Akash Yadav --- subprojects/kotlin-analysis-api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subprojects/kotlin-analysis-api/build.gradle.kts b/subprojects/kotlin-analysis-api/build.gradle.kts index 6e11cc1ce0..57fe554dae 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}-f1ac8b3" +val ktAndroidTag = "v${ktAndroidVersion}-a98fda0" 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 = "8c7cad7e0905a861048cce000c3ef22d9ad05572b4f9a0830e0c0e0060ddd3c9", + sha256Checksum = "804781ae6c6cdbc5af1ca9a08959af9552395d48704a6c5fcb43b5516cb3e378", ) } } From b208658bc1ea471871096818a723f3a7bca76f1b Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 24 Mar 2026 21:38:58 +0530 Subject: [PATCH 06/58] fix: replace usages of Unsafe with UnsafeImpl Signed-off-by: Akash Yadav --- app/build.gradle.kts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d9dcb121a5..554718fb66 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -205,6 +205,18 @@ desugaring { EnvUtil::logbackVersion.javaMethod!!, DesugarEnvUtil::logbackVersion.javaMethod!!, ) + + // Replace usages of Unsafe class (from com.intellij.util.containers) + // with our own implementation + // The original implementation uses MethodHandle instances to access APIs + // from sun.misc.Unsafe which are not directly accessible on Android + // As a result, we have our implementatio of that class which makes use + // of HiddenApiBypass to access the same methods, and provides a drop-in + // replacement of the original class + replaceClass( + "org.jetbrains.kotlin.com.intellij.util.containers.Unsafe", + "org.jetbrains.kotlin.com.intellij.util.containers.UnsafeImpl", + ) } } From c844ad29236081068288393223ff1684bceaacd6 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Mon, 23 Mar 2026 20:18:36 +0530 Subject: [PATCH 07/58] fix: make Kotlin LSP no-op Signed-off-by: Akash Yadav --- lsp/kotlin/build.gradle.kts | 17 - .../lsp/kotlin/KotlinLanguageClientBridge.kt | 184 ---------- .../lsp/kotlin/KotlinLanguageServer.kt | 314 +----------------- .../lsp/kotlin/KotlinServerSettings.kt | 12 +- .../lsp/kotlin/adapters/ModelConverters.kt | 196 ----------- .../androidide/lsp/models/Definitions.kt | 6 +- .../androidide/lsp/models/References.kt | 6 +- .../androidide/lsp/models/Signatures.kt | 12 +- 8 files changed, 40 insertions(+), 707 deletions(-) delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageClientBridge.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/adapters/ModelConverters.kt diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index d111677334..8af6820538 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -25,21 +25,6 @@ plugins { android { namespace = "${BuildConfig.PACKAGE_NAME}.lsp.kotlin" - - sourceSets { - named("main") { - resources.srcDir( - project(":lsp:kotlin-stdlib-generator") - .layout.buildDirectory.dir("generated-resources/stdlib") - ) - } - } -} - -afterEvaluate { - tasks.matching { it.name.startsWith("process") && it.name.endsWith("JavaRes") }.configureEach { - dependsOn(":lsp:kotlin-stdlib-generator:generateStdlibIndex") - } } kapt { @@ -51,7 +36,6 @@ kapt { dependencies { kapt(projects.annotationProcessors) - implementation(projects.lsp.kotlinCore) implementation(projects.lsp.api) implementation(projects.lsp.models) implementation(projects.eventbusEvents) @@ -60,7 +44,6 @@ dependencies { implementation(projects.subprojects.projects) implementation(projects.subprojects.projectModels) - implementation(libs.common.lsp4j) implementation(libs.common.jsonrpc) implementation(libs.common.kotlin) implementation(libs.common.kotlin.coroutines.core) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageClientBridge.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageClientBridge.kt deleted file mode 100644 index 44ceca27cc..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageClientBridge.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.lsp.kotlin - -import com.itsaky.androidide.lsp.api.ILanguageClient -import com.itsaky.androidide.lsp.models.DiagnosticResult -import org.eclipse.lsp4j.MessageActionItem -import org.eclipse.lsp4j.MessageParams -import org.eclipse.lsp4j.PublishDiagnosticsParams -import org.eclipse.lsp4j.ShowMessageRequestParams -import org.eclipse.lsp4j.services.LanguageClient -import org.slf4j.LoggerFactory -import java.net.URI -import java.nio.file.Paths -import java.util.concurrent.CompletableFuture - -typealias PositionToOffsetResolver = (uri: String) -> ((line: Int, column: Int) -> Int)? - -class KotlinLanguageClientBridge( - private val ideClient: ILanguageClient, - private val positionResolver: PositionToOffsetResolver -) : LanguageClient { - - companion object { - private val log = LoggerFactory.getLogger(KotlinLanguageClientBridge::class.java) - } - - override fun telemetryEvent(obj: Any?) { - } - - override fun publishDiagnostics(diagnostics: PublishDiagnosticsParams) { - log.info("[DIAG-DEBUG] publishDiagnostics: uri={}, count={}", diagnostics.uri, diagnostics.diagnostics.size) - - val path = try { - Paths.get(URI(diagnostics.uri)) - } catch (e: Exception) { - Paths.get(diagnostics.uri) - } - - val positionToOffset = positionResolver(diagnostics.uri) ?: run { - log.warn("[DIAG-DEBUG] Position resolver NULL for: {}, using fallback", diagnostics.uri) - createFallbackPositionCalculator(path) - } - - if (positionToOffset == null) { - log.error("[DIAG-DEBUG] No resolver, dropping {} diagnostics for: {}", diagnostics.diagnostics.size, diagnostics.uri) - return - } - - val diagnosticItems = diagnostics.diagnostics.mapNotNull { diag -> - try { - val startIndex = positionToOffset(diag.range.start.line, diag.range.start.character) - val endIndex = positionToOffset(diag.range.end.line, diag.range.end.character) - - val expectedColSpan = if (diag.range.start.line == diag.range.end.line) { - diag.range.end.character - diag.range.start.character - } else { - -1 - } - val actualIndexSpan = endIndex - startIndex - - log.info("[DIAG-DEBUG] range={}:{}-{}:{} -> idx={}-{} (colSpan={}, idxSpan={}) '{}'", - diag.range.start.line, diag.range.start.character, - diag.range.end.line, diag.range.end.character, - startIndex, endIndex, - expectedColSpan, actualIndexSpan, - diag.message.take(50) - ) - - if (expectedColSpan >= 0 && actualIndexSpan != expectedColSpan) { - log.warn("[DIAG-DEBUG] MISMATCH! idxSpan={} != colSpan={}", actualIndexSpan, expectedColSpan) - } - - val startPos = com.itsaky.androidide.models.Position( - diag.range.start.line, - diag.range.start.character, - startIndex - ) - val endPos = com.itsaky.androidide.models.Position( - diag.range.end.line, - diag.range.end.character, - endIndex - ) - - com.itsaky.androidide.lsp.models.DiagnosticItem( - diag.message, - diag.code?.left ?: diag.code?.right?.toString() ?: "", - com.itsaky.androidide.models.Range(startPos, endPos), - diag.source ?: "ktlsp", - when (diag.severity) { - org.eclipse.lsp4j.DiagnosticSeverity.Error -> - com.itsaky.androidide.lsp.models.DiagnosticSeverity.ERROR - org.eclipse.lsp4j.DiagnosticSeverity.Warning -> - com.itsaky.androidide.lsp.models.DiagnosticSeverity.WARNING - org.eclipse.lsp4j.DiagnosticSeverity.Information -> - com.itsaky.androidide.lsp.models.DiagnosticSeverity.INFO - org.eclipse.lsp4j.DiagnosticSeverity.Hint -> - com.itsaky.androidide.lsp.models.DiagnosticSeverity.HINT - null -> com.itsaky.androidide.lsp.models.DiagnosticSeverity.INFO - } - ) - } catch (e: Exception) { - log.error("Error converting diagnostic: ${diag.message}", e) - null - } - } - - val result = DiagnosticResult(path, diagnosticItems) - log.info("[DIAG-DEBUG] Publishing {} diagnostics to IDE", diagnosticItems.size) - ideClient.publishDiagnostics(result) - } - - private fun createFallbackPositionCalculator(path: java.nio.file.Path): ((Int, Int) -> Int)? { - return try { - val file = path.toFile() - if (!file.exists() || !file.isFile) { - log.warn("File does not exist for fallback position calculation: {}", path) - return null - } - - val content = file.readText() - val lineOffsets = mutableListOf() - lineOffsets.add(0) - - var offset = 0 - for (char in content) { - offset++ - if (char == '\n') { - lineOffsets.add(offset) - } - } - - log.info("Created fallback position calculator for {} with {} lines", path, lineOffsets.size) - - val calculator: (Int, Int) -> Int = { line, column -> - if (line < lineOffsets.size) { - lineOffsets[line] + column - } else { - content.length - } - } - calculator - } catch (e: Exception) { - log.error("Error creating fallback position calculator for {}: {}", path, e.message) - null - } - } - - override fun showMessage(messageParams: MessageParams) { - log.info("Kotlin LSP: ${messageParams.message}") - } - - override fun showMessageRequest( - requestParams: ShowMessageRequestParams - ): CompletableFuture { - log.info("Kotlin LSP request: ${requestParams.message}") - return CompletableFuture.completedFuture(null) - } - - override fun logMessage(message: MessageParams) { - when (message.type) { - org.eclipse.lsp4j.MessageType.Error -> log.error("Kotlin LSP: ${message.message}") - org.eclipse.lsp4j.MessageType.Warning -> log.warn("Kotlin LSP: ${message.message}") - org.eclipse.lsp4j.MessageType.Info -> log.info("Kotlin LSP: ${message.message}") - org.eclipse.lsp4j.MessageType.Log -> log.debug("Kotlin LSP: ${message.message}") - null -> log.debug("Kotlin LSP: ${message.message}") - } - } -} 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 fc5cf4e92a..dba45c6bf3 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 @@ -17,8 +17,6 @@ package com.itsaky.androidide.lsp.kotlin -import androidx.core.net.toUri -import com.itsaky.androidide.eventbus.events.editor.ChangeType import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent @@ -26,8 +24,6 @@ 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.adapters.toIde -import com.itsaky.androidide.lsp.kotlin.adapters.toLsp4j import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.DefinitionParams @@ -39,26 +35,8 @@ import com.itsaky.androidide.lsp.models.ReferenceResult import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams import com.itsaky.androidide.models.Range -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 com.itsaky.androidide.projects.models.projectDir import com.itsaky.androidide.utils.DocumentUtils -import java.io.File -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.appdevforall.codeonthego.lsp.kotlin.server.KotlinLanguageServer as KtLspServer -import org.eclipse.lsp4j.DidChangeTextDocumentParams -import org.eclipse.lsp4j.DidCloseTextDocumentParams -import org.eclipse.lsp4j.DidOpenTextDocumentParams -import org.eclipse.lsp4j.InitializeParams -import org.eclipse.lsp4j.TextDocumentContentChangeEvent -import org.eclipse.lsp4j.TextDocumentIdentifier -import org.eclipse.lsp4j.TextDocumentItem -import org.eclipse.lsp4j.VersionedTextDocumentIdentifier import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -67,9 +45,7 @@ import java.nio.file.Path class KotlinLanguageServer : ILanguageServer { - private val ktLspServer = KtLspServer() - private var clientBridge: KotlinLanguageClientBridge? = null - private var _client: ILanguageClient? = null + private var _client: ILanguageClient? = null private var _settings: IServerSettings? = null private var selectedFile: Path? = null private var initialized = false @@ -96,203 +72,52 @@ class KotlinLanguageServer : ILanguageServer { } override fun shutdown() { - ktLspServer.shutdown().get() EventBus.getDefault().unregister(this) initialized = false } override fun connectClient(client: ILanguageClient?) { this._client = client - if (client != null) { - val positionResolver: PositionToOffsetResolver = { uri -> - val normalizedUri = normalizeUri(uri) - val state = ktLspServer.getDocumentManager().get(normalizedUri) - if (state == null) { - log.debug("positionResolver: no document state for URI: {} (normalized: {})", uri, normalizedUri) - } - state?.let { it::positionToOffset } - } - clientBridge = KotlinLanguageClientBridge(client, positionResolver) - ktLspServer.connect(clientBridge!!) - } - } - - private fun normalizeUri(uri: String): String { - return try { - java.net.URI(uri).normalize().toString() - } catch (e: Exception) { - uri - } } - override fun applySettings(settings: IServerSettings?) { + override fun applySettings(settings: IServerSettings?) { this._settings = settings } override fun setupWithProject(workspace: Workspace) { log.info("setupWithProject called, initialized={}", initialized) if (!initialized) { - loadStdlibIndex() - - val initParams = InitializeParams().apply { - rootUri = workspace.rootProject.projectDir.toUri().toString() - } - ktLspServer.initialize(initParams).get() - ktLspServer.initialized(null) - log.info("Kotlin LSP initialized with stdlib index") initialized = true } - - indexClasspaths(workspace) } - private fun loadStdlibIndex() { - try { - val startTime = System.currentTimeMillis() - val stdlibStream = javaClass.getResourceAsStream("/stdlib-index.json") - if (stdlibStream != null) { - stdlibStream.use { inputStream -> - val stdlibIndex = org.appdevforall.codeonthego.lsp.kotlin.index.StdlibIndexLoader.loadFromStream(inputStream) - ktLspServer.loadStdlibIndex(stdlibIndex) - val elapsed = System.currentTimeMillis() - startTime - log.info("Loaded stdlib index: {} symbols in {}ms", stdlibIndex.size, elapsed) - } - } else { - log.warn("stdlib-index.json not found in resources, using minimal index") - } - } catch (e: Exception) { - log.error("Failed to load stdlib-index.json, using minimal index", e) - - } - } - - private fun indexClasspaths(workspace: Workspace) { - log.info("indexClasspaths called, subProjects count={}", workspace.subProjects.size) - CoroutineScope(Dispatchers.IO).launch { - try { - val classpaths = mutableSetOf() - val bootClasspaths = mutableSetOf() - - for (project in workspace.subProjects) { - log.debug("Checking project: {} (type={})", project.name, project::class.simpleName) - if (project is ModuleProject) { - val projectClasspaths = project.getCompileClasspaths() - log.debug("Project {} has {} classpath entries", project.name, projectClasspaths.size) - classpaths.addAll(projectClasspaths) - - if (project is AndroidModule) { - val projectBootClasspaths = project.bootClassPaths - log.debug("Project {} has {} boot classpath entries", project.name, projectBootClasspaths.size) - bootClasspaths.addAll(projectBootClasspaths) - } - } - } - - classpaths.addAll(bootClasspaths.filter { it.exists() }) - - log.info("Total classpath entries found: {} (including {} boot classpaths)", classpaths.size, bootClasspaths.size) - if (classpaths.isNotEmpty()) { - val files = classpaths.filter { it.exists() } - log.info("Indexing {} existing classpath entries for Kotlin LSP", files.size) - ktLspServer.setClasspathAsync(files).thenAccept { index -> - log.info("Kotlin LSP classpath indexed: {} symbols from {} jars", index.size, index.jarCount) - }.exceptionally { e -> - log.error("Error in classpath indexing async", e) - null - } - } else { - log.warn("No classpath entries found for Kotlin LSP") - } - } catch (e: Exception) { - log.error("Error indexing classpaths for Kotlin LSP", e) - } - } - } override fun complete(params: CompletionParams?): CompletionResult { - log.debug("complete() called, params={}", params != null) - if (params == null || !settings.completionsEnabled()) { - log.debug("complete() returning EMPTY: params={}, completionsEnabled={}", params != null, settings.completionsEnabled()) - return CompletionResult.EMPTY - } - - if (!DocumentUtils.isKotlinFile(params.file)) { - log.debug("complete() returning EMPTY: not a Kotlin file") - return CompletionResult.EMPTY - } - - val uri = params.file.toUri().toString() - - ktLspServer.getAnalysisScheduler().analyzeSync(uri) - - log.debug("complete() uri={}, position={}:{}, prefix={}", uri, params.position.line, params.position.column, params.prefix) - val lspParams = org.eclipse.lsp4j.CompletionParams().apply { - textDocument = TextDocumentIdentifier(uri) - position = params.position.toLsp4j() - } - - return try { - val future = ktLspServer.textDocumentService.completion(lspParams) - val result = future.get() - val items = result?.right?.items ?: result?.left ?: emptyList() - log.debug("complete() got {} items from ktlsp", items.size) - CompletionResult(items.map { it.toIde(params.prefix ?: "") }) - } catch (e: Exception) { - log.error("Error during completion", e) - CompletionResult.EMPTY - } + return CompletionResult.EMPTY } override suspend fun findReferences(params: ReferenceParams): ReferenceResult { if (!settings.referencesEnabled()) { - return ReferenceResult(emptyList()) + return ReferenceResult.empty() } if (!DocumentUtils.isKotlinFile(params.file)) { - return ReferenceResult(emptyList()) - } - - val uri = params.file.toUri().toString() - val lspParams = org.eclipse.lsp4j.ReferenceParams().apply { - textDocument = TextDocumentIdentifier(uri) - position = params.position.toLsp4j() - context = org.eclipse.lsp4j.ReferenceContext(params.includeDeclaration) + return ReferenceResult.empty() } - return try { - val future = ktLspServer.textDocumentService.references(lspParams) - val locations = future.get() ?: emptyList() - ReferenceResult(locations.map { it.toIde() }) - } catch (e: Exception) { - log.error("Error finding references", e) - ReferenceResult(emptyList()) - } + return ReferenceResult.empty() } override suspend fun findDefinition(params: DefinitionParams): DefinitionResult { if (!settings.definitionsEnabled()) { - return DefinitionResult(emptyList()) + return DefinitionResult.empty() } if (!DocumentUtils.isKotlinFile(params.file)) { - return DefinitionResult(emptyList()) - } - - val uri = params.file.toUri().toString() - val lspParams = org.eclipse.lsp4j.DefinitionParams().apply { - textDocument = TextDocumentIdentifier(uri) - position = params.position.toLsp4j() + return DefinitionResult.empty() } - return try { - val future = ktLspServer.textDocumentService.definition(lspParams) - val result = future.get() - val locations = result?.left ?: emptyList() - DefinitionResult(locations.map { it.toIde() }) - } catch (e: Exception) { - log.error("Error finding definition", e) - DefinitionResult(emptyList()) - } + return DefinitionResult.empty() } override suspend fun expandSelection(params: ExpandSelectionParams): Range { @@ -301,31 +126,18 @@ class KotlinLanguageServer : ILanguageServer { override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp { if (!settings.signatureHelpEnabled()) { - return SignatureHelp(emptyList(), -1, -1) + return SignatureHelp.empty() } if (!DocumentUtils.isKotlinFile(params.file)) { - return SignatureHelp(emptyList(), -1, -1) - } - - val uri = params.file.toUri().toString() - val lspParams = org.eclipse.lsp4j.SignatureHelpParams().apply { - textDocument = TextDocumentIdentifier(uri) - position = params.position.toLsp4j() + return SignatureHelp.empty() } - return try { - val future = ktLspServer.textDocumentService.signatureHelp(lspParams) - val result = future.get() - result?.toIde() ?: SignatureHelp(emptyList(), -1, -1) - } catch (e: Exception) { - log.error("Error getting signature help", e) - SignatureHelp(emptyList(), -1, -1) - } + return SignatureHelp.empty() } override suspend fun analyze(file: Path): DiagnosticResult { - log.debug("analyze() called for file: {}", file) + log.debug("analyze(file={})", file) if (!settings.diagnosticsEnabled() || !settings.codeAnalysisEnabled()) { log.debug("analyze() skipped: diagnosticsEnabled={}, codeAnalysisEnabled={}", @@ -338,19 +150,7 @@ class KotlinLanguageServer : ILanguageServer { return DiagnosticResult.NO_UPDATE } - val uri = file.toUri().toString() - val state = ktLspServer.getDocumentManager().get(uri) - if (state == null) { - log.warn("analyze() skipped: document state not found for URI: {}", uri) - return DiagnosticResult.NO_UPDATE - } - - ktLspServer.getAnalysisScheduler().analyzeSync(uri) - - val diagnostics = state.diagnostics - log.info("analyze() completed: {} diagnostics found for {}", diagnostics.size, file.fileName) - - return DiagnosticResult(file, diagnostics.map { it.toIde(state::positionToOffset) }) + return DiagnosticResult.NO_UPDATE } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -361,17 +161,7 @@ class KotlinLanguageServer : ILanguageServer { } selectedFile = event.openedFile - val uri = event.openedFile.toUri().toString() - - log.debug("onDocumentOpen: uri={}, version={}, textLen={}", uri, event.version, event.text.length) - - val params = DidOpenTextDocumentParams().apply { - textDocument = TextDocumentItem(uri, "kotlin", event.version, event.text) - } - ktLspServer.textDocumentService.didOpen(params) - - analyzeCurrentFileAsync() - } + } @Subscribe(threadMode = ThreadMode.ASYNC) @Suppress("unused") @@ -379,35 +169,6 @@ class KotlinLanguageServer : ILanguageServer { if (!DocumentUtils.isKotlinFile(event.changedFile)) { return } - - val uri = event.changedFile.toUri().toString() - - log.debug("onDocumentChange: uri={}, version={}, changeType={}", uri, event.version, event.changeType) - log.debug(" changeRange={}, changedText='{}', newText.len={}", - event.changeRange, event.changedText, event.newText?.length ?: -1) - - val changeText = when (event.changeType) { - ChangeType.DELETE -> "" - else -> event.changedText - } - - val startIndex = event.changeRange.start.index - val endIndex = if (event.changeType == ChangeType.INSERT) { - startIndex - } else { - event.changeRange.end.index - } - - log.debug(" using index-based sync: indices=$startIndex-$endIndex (adjusted for {}), text='{}' ({} chars)", - event.changeType, changeText, changeText.length) - - ktLspServer.didChangeByIndex( - uri = uri, - startIndex = startIndex, - endIndex = endIndex, - newText = changeText, - version = event.version - ) } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -416,16 +177,6 @@ class KotlinLanguageServer : ILanguageServer { if (!DocumentUtils.isKotlinFile(event.closedFile)) { return } - - val uri = event.closedFile.toUri().toString() - val params = DidCloseTextDocumentParams().apply { - textDocument = TextDocumentIdentifier(uri) - } - ktLspServer.textDocumentService.didClose(params) - - if (selectedFile == event.closedFile) { - selectedFile = null - } } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -439,40 +190,5 @@ class KotlinLanguageServer : ILanguageServer { val uri = event.selectedFile.toUri().toString() log.debug("onDocumentSelected: uri={}", uri) - - val existingState = ktLspServer.getDocumentManager().get(uri) - if (existingState == null) { - log.info("onDocumentSelected: document not open in KtLsp, opening it first: {}", uri) - log.debug(" available uris: {}", ktLspServer.getDocumentManager().openUris.take(5)) - try { - val content = event.selectedFile.toFile().readText() - log.debug(" read {} chars from disk", content.length) - val params = DidOpenTextDocumentParams().apply { - textDocument = TextDocumentItem(uri, "kotlin", 0, content) - } - ktLspServer.textDocumentService.didOpen(params) - } catch (e: Exception) { - log.error("Failed to open document in KtLsp: {}", uri, e) - } - } else { - log.debug("onDocumentSelected: document already open, version={}, contentLen={}", - existingState.version, existingState.content.length) - } - - analyzeCurrentFileAsync() - } - - private fun analyzeCurrentFileAsync() { - val file = selectedFile ?: return - val client = _client ?: return - - CoroutineScope(Dispatchers.Default).launch { - val result = analyze(file) - if (result != DiagnosticResult.NO_UPDATE) { - withContext(Dispatchers.Main) { - client.publishDiagnostics(result) - } - } - } } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinServerSettings.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinServerSettings.kt index 6d1019d3cc..fa51c56c7f 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinServerSettings.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinServerSettings.kt @@ -24,14 +24,12 @@ class KotlinServerSettings private constructor() : PrefBasedServerSettings() { override fun diagnosticsEnabled(): Boolean = true companion object { - private var instance: KotlinServerSettings? = null + private val _instance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + KotlinServerSettings() + } @JvmStatic - fun getInstance(): KotlinServerSettings { - if (instance == null) { - instance = KotlinServerSettings() - } - return instance!! - } + fun getInstance(): KotlinServerSettings = + _instance } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/adapters/ModelConverters.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/adapters/ModelConverters.kt deleted file mode 100644 index 498821dc75..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/adapters/ModelConverters.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.lsp.kotlin.adapters - -import android.util.Log -import com.itsaky.androidide.lsp.models.CompletionItem -import com.itsaky.androidide.lsp.models.CompletionItemKind -import com.itsaky.androidide.lsp.models.DiagnosticItem -import com.itsaky.androidide.lsp.models.DiagnosticSeverity -import com.itsaky.androidide.lsp.models.InsertTextFormat -import com.itsaky.androidide.lsp.models.MarkupContent -import com.itsaky.androidide.lsp.models.MarkupKind -import com.itsaky.androidide.lsp.models.MatchLevel -import com.itsaky.androidide.lsp.models.ParameterInformation -import com.itsaky.androidide.lsp.models.SignatureHelp -import com.itsaky.androidide.lsp.models.SignatureInformation -import com.itsaky.androidide.models.Location -import com.itsaky.androidide.models.Position -import com.itsaky.androidide.models.Range -import org.appdevforall.codeonthego.lsp.kotlin.semantic.Diagnostic -import org.appdevforall.codeonthego.lsp.kotlin.semantic.DiagnosticSeverity as KtDiagnosticSeverity -import java.net.URI -import java.nio.file.Paths -import org.eclipse.lsp4j.CompletionItemKind as Lsp4jCompletionItemKind -import org.eclipse.lsp4j.DiagnosticSeverity as Lsp4jDiagnosticSeverity -import org.eclipse.lsp4j.InsertTextFormat as Lsp4jInsertTextFormat -import org.eclipse.lsp4j.Location as Lsp4jLocation -import org.eclipse.lsp4j.MarkupKind as Lsp4jMarkupKind -import org.eclipse.lsp4j.Position as Lsp4jPosition -import org.eclipse.lsp4j.Range as Lsp4jRange -import org.eclipse.lsp4j.SignatureHelp as Lsp4jSignatureHelp -import org.eclipse.lsp4j.CompletionItem as Lsp4jCompletionItem - -fun Position.toLsp4j(): Lsp4jPosition = Lsp4jPosition(line, column) - -fun Lsp4jPosition.toIde(): Position = Position(line, character) - -fun Range.toLsp4j(): Lsp4jRange = Lsp4jRange(start.toLsp4j(), end.toLsp4j()) - -fun Lsp4jRange.toIde(): Range = Range(start.toIde(), end.toIde()) - -fun Lsp4jLocation.toIde(): Location { - val path = try { - Paths.get(URI(uri)) - } catch (e: Exception) { - Paths.get(uri) - } - return Location(path, range.toIde()) -} - -fun Lsp4jCompletionItem.toIde(prefix: String): CompletionItem { - val matchLevel = CompletionItem.matchLevel(label, prefix) - - return CompletionItem( - label, - detail ?: "", - insertText, - insertTextFormat?.toIde(), - sortText, - null, - kind?.toIde() ?: CompletionItemKind.NONE, - matchLevel, - null, - null - ) -} - -fun Lsp4jCompletionItemKind.toIde(): CompletionItemKind { - return when (this) { - Lsp4jCompletionItemKind.Text -> CompletionItemKind.NONE - Lsp4jCompletionItemKind.Method -> CompletionItemKind.METHOD - Lsp4jCompletionItemKind.Function -> CompletionItemKind.FUNCTION - Lsp4jCompletionItemKind.Constructor -> CompletionItemKind.CONSTRUCTOR - Lsp4jCompletionItemKind.Field -> CompletionItemKind.FIELD - Lsp4jCompletionItemKind.Variable -> CompletionItemKind.VARIABLE - Lsp4jCompletionItemKind.Class -> CompletionItemKind.CLASS - Lsp4jCompletionItemKind.Interface -> CompletionItemKind.INTERFACE - Lsp4jCompletionItemKind.Module -> CompletionItemKind.MODULE - Lsp4jCompletionItemKind.Property -> CompletionItemKind.PROPERTY - Lsp4jCompletionItemKind.Keyword -> CompletionItemKind.KEYWORD - Lsp4jCompletionItemKind.Snippet -> CompletionItemKind.SNIPPET - Lsp4jCompletionItemKind.Value -> CompletionItemKind.VALUE - Lsp4jCompletionItemKind.EnumMember -> CompletionItemKind.ENUM_MEMBER - Lsp4jCompletionItemKind.Enum -> CompletionItemKind.ENUM - Lsp4jCompletionItemKind.TypeParameter -> CompletionItemKind.TYPE_PARAMETER - else -> CompletionItemKind.NONE - } -} - -fun Lsp4jInsertTextFormat.toIde(): InsertTextFormat { - return when (this) { - Lsp4jInsertTextFormat.PlainText -> InsertTextFormat.PLAIN_TEXT - Lsp4jInsertTextFormat.Snippet -> InsertTextFormat.SNIPPET - } -} - -fun Diagnostic.toIde(positionToOffset: (line: Int, column: Int) -> Int): DiagnosticItem { - val startIndex = if (range.hasOffsets) { - range.startOffset - } else { - positionToOffset(range.startLine, range.startColumn) - } - val endIndex = if (range.hasOffsets) { - range.endOffset - } else { - positionToOffset(range.endLine, range.endColumn) - } - - Log.i("DIAG-DEBUG", "range=${range.startLine}:${range.startColumn}-${range.endLine}:${range.endColumn} -> idx=$startIndex-$endIndex (hasOffsets=${range.hasOffsets}) '${message.take(50)}'") - - return DiagnosticItem( - message, - code.name, - Range( - Position(range.startLine, range.startColumn, startIndex), - Position(range.endLine, range.endColumn, endIndex) - ), - "ktlsp", - severity.toIde() - ) -} - -fun KtDiagnosticSeverity.toIde(): DiagnosticSeverity { - return when (this) { - KtDiagnosticSeverity.ERROR -> DiagnosticSeverity.ERROR - KtDiagnosticSeverity.WARNING -> DiagnosticSeverity.WARNING - KtDiagnosticSeverity.INFO -> DiagnosticSeverity.INFO - KtDiagnosticSeverity.HINT -> DiagnosticSeverity.HINT - } -} - -fun Lsp4jSignatureHelp.toIde(): SignatureHelp { - return SignatureHelp( - signatures.map { it.toIde() }, - activeSignature ?: -1, - activeParameter ?: -1 - ) -} - -fun org.eclipse.lsp4j.SignatureInformation.toIde(): SignatureInformation { - val doc = when { - documentation?.isRight == true -> { - val markup = documentation.right - MarkupContent( - markup.value, - when (markup.kind) { - Lsp4jMarkupKind.MARKDOWN -> MarkupKind.MARKDOWN - else -> MarkupKind.PLAIN - } - ) - } - documentation?.isLeft == true -> MarkupContent(documentation.left, MarkupKind.PLAIN) - else -> MarkupContent() - } - - return SignatureInformation( - label, - doc, - parameters?.map { it.toIde() } ?: emptyList() - ) -} - -fun org.eclipse.lsp4j.ParameterInformation.toIde(): ParameterInformation { - val labelStr = if (label.isLeft) label.left else "${label.right.first}-${label.right.second}" - val doc = when { - documentation?.isRight == true -> { - val markup = documentation.right - MarkupContent( - markup.value, - when (markup.kind) { - Lsp4jMarkupKind.MARKDOWN -> MarkupKind.MARKDOWN - else -> MarkupKind.PLAIN - } - ) - } - documentation?.isLeft == true -> MarkupContent(documentation.left, MarkupKind.PLAIN) - else -> MarkupContent() - } - - return ParameterInformation(labelStr, doc) -} diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Definitions.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Definitions.kt index 7e473d63f8..51dcd66689 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Definitions.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Definitions.kt @@ -30,4 +30,8 @@ data class DefinitionParams( override val cancelChecker: ICancelChecker ) : CancellableRequestParams -data class DefinitionResult(var locations: List) +data class DefinitionResult(var locations: List) { + companion object { + fun empty() = DefinitionResult(locations = emptyList()) + } +} diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/References.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/References.kt index d73505af23..e1320ec5d2 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/References.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/References.kt @@ -31,4 +31,8 @@ data class ReferenceParams( override val cancelChecker: ICancelChecker ) : CancellableRequestParams -data class ReferenceResult(var locations: List) +data class ReferenceResult(var locations: List) { + companion object { + fun empty() = ReferenceResult(locations = emptyList()) + } +} diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Signatures.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Signatures.kt index deadc55d64..c5219e211e 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Signatures.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Signatures.kt @@ -21,7 +21,7 @@ import com.itsaky.androidide.lsp.CancellableRequestParams import com.itsaky.androidide.models.Position import com.itsaky.androidide.progress.ICancelChecker import java.nio.file.Path -import java.util.* +import java.util.Collections data class ParameterInformation(var label: String, var documentation: MarkupContent) { constructor() : this("", MarkupContent()) @@ -39,7 +39,15 @@ data class SignatureHelp( var signatures: List, var activeSignature: Int, var activeParameter: Int -) +) { + companion object { + fun empty() = SignatureHelp( + signatures = emptyList(), + activeSignature = -1, + activeParameter = -1, + ) + } +} data class SignatureHelpParams( var file: Path, From 58db2cbecb1adb6b4a0d93b076fa462e020651d1 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 24 Mar 2026 18:31:29 +0530 Subject: [PATCH 08/58] feat: configure K2 standalone session when setting up LSP Signed-off-by: Akash Yadav --- .../configuration/IJdkDistributionProvider.kt | 91 ++--- .../lsp/kotlin/KotlinLanguageServer.kt | 338 +++++++++++------- .../kotlin/compiler/CompilationEnvironment.kt | 75 ++++ .../lsp/kotlin/compiler/CompilationKind.kt | 16 + .../lsp/kotlin/compiler/Compiler.kt | 79 ++++ .../lsp/kotlin/compiler/CompilerExts.kt | 10 + .../androidide/projects/api/AndroidModule.kt | 43 ++- .../androidide/projects/api/JavaModule.kt | 21 +- .../androidide/projects/api/ModuleProject.kt | 5 +- 9 files changed, 499 insertions(+), 179 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt diff --git a/common/src/main/java/com/itsaky/androidide/app/configuration/IJdkDistributionProvider.kt b/common/src/main/java/com/itsaky/androidide/app/configuration/IJdkDistributionProvider.kt index 5aa03ff595..d754dce9d4 100644 --- a/common/src/main/java/com/itsaky/androidide/app/configuration/IJdkDistributionProvider.kt +++ b/common/src/main/java/com/itsaky/androidide/app/configuration/IJdkDistributionProvider.kt @@ -28,55 +28,60 @@ import com.itsaky.androidide.utils.ServiceLoader */ interface IJdkDistributionProvider { - /** - * The list of JDK distributions installed on the device. - */ - val installedDistributions: List + /** + * The list of JDK distributions installed on the device. + */ + val installedDistributions: List - /** - * Reloads the installed JDK distributions. This function is synchronous and should not be called - * on the UI thread. - */ - @WorkerThread - fun loadDistributions() + /** + * Reloads the installed JDK distributions. This function is synchronous and should not be called + * on the UI thread. + */ + @WorkerThread + fun loadDistributions() - /** - * Get the [JdkDistribution] instance for the given java version. - * - * @return The [JdkDistribution] instance for the given java version, or `null` if no such - * distribution is found. - */ - fun forVersion(javaVersion: String) : JdkDistribution? = - installedDistributions.firstOrNull { it.javaVersion == javaVersion } + /** + * Get the [JdkDistribution] instance for the given java version. + * + * @return The [JdkDistribution] instance for the given java version, or `null` if no such + * distribution is found. + */ + fun forVersion(javaVersion: String): JdkDistribution? = + installedDistributions.firstOrNull { it.javaVersion == javaVersion } - /** - * Get the [JdkDistribution] instance for the given java home. - * - * @return The [JdkDistribution] instance for the given java home, or `null` if no such - * distribution is found. - */ - fun forJavaHome(javaHome: String) : JdkDistribution? = - installedDistributions.firstOrNull { it.javaHome == javaHome } + /** + * Get the [JdkDistribution] instance for the given java home. + * + * @return The [JdkDistribution] instance for the given java home, or `null` if no such + * distribution is found. + */ + fun forJavaHome(javaHome: String): JdkDistribution? = + installedDistributions.firstOrNull { it.javaHome == javaHome } - companion object { + companion object { - /** - * The default java version. - */ - const val DEFAULT_JAVA_VERSION = "17" + /** + * The default Java version. + */ + const val DEFAULT_JAVA_RELEASE = 21 - private val _instance by lazy { - ServiceLoader.load( - IJdkDistributionProvider::class.java, - IJdkDistributionProvider::class.java.classLoader - ).findFirstOrThrow() - } + /** + * The default java version. + */ + const val DEFAULT_JAVA_VERSION = DEFAULT_JAVA_RELEASE.toString() - /** - * Get instance of [IJdkDistributionProvider]. - */ - @JvmStatic - fun getInstance(): IJdkDistributionProvider = _instance - } + private val _instance by lazy { + ServiceLoader.load( + IJdkDistributionProvider::class.java, + IJdkDistributionProvider::class.java.classLoader + ).findFirstOrThrow() + } + + /** + * Get instance of [IJdkDistributionProvider]. + */ + @JvmStatic + fun getInstance(): IJdkDistributionProvider = _instance + } } \ No newline at end of file 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 dba45c6bf3..bf9275787f 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 @@ -17,6 +17,8 @@ package com.itsaky.androidide.lsp.kotlin +import com.itsaky.androidide.app.BaseApplication +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 @@ -24,6 +26,7 @@ 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.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.DefinitionParams @@ -35,160 +38,255 @@ import com.itsaky.androidide.lsp.models.ReferenceResult import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams import com.itsaky.androidide.models.Range +import com.itsaky.androidide.projects.api.ModuleProject import com.itsaky.androidide.projects.api.Workspace import com.itsaky.androidide.utils.DocumentUtils +import com.itsaky.androidide.utils.Environment import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.config.LanguageVersion +import org.jetbrains.kotlin.platform.jvm.JdkPlatform +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms import org.slf4j.LoggerFactory import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.pathString class KotlinLanguageServer : ILanguageServer { private var _client: ILanguageClient? = null - private var _settings: IServerSettings? = null - private var selectedFile: Path? = null - private var initialized = false + private var _settings: IServerSettings? = null + private var selectedFile: Path? = null + private var initialized = false - override val serverId: String = SERVER_ID + private var compiler: Compiler? = null - override val client: ILanguageClient? - get() = _client + override val serverId: String = SERVER_ID - val settings: IServerSettings - get() = _settings ?: KotlinServerSettings.getInstance().also { _settings = it } + override val client: ILanguageClient? + get() = _client - companion object { - const val SERVER_ID = "ide.lsp.kotlin" - private val log = LoggerFactory.getLogger(KotlinLanguageServer::class.java) - } + val settings: IServerSettings + get() = _settings ?: KotlinServerSettings.getInstance().also { _settings = it } - init { - applySettings(KotlinServerSettings.getInstance()) + companion object { + const val SERVER_ID = "ide.lsp.kotlin" + private val log = LoggerFactory.getLogger(KotlinLanguageServer::class.java) + } + + init { + applySettings(KotlinServerSettings.getInstance()) - if (!EventBus.getDefault().isRegistered(this)) { - EventBus.getDefault().register(this) - } - } + if (!EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().register(this) + } + } - override fun shutdown() { - EventBus.getDefault().unregister(this) - initialized = false - } + override fun shutdown() { + EventBus.getDefault().unregister(this) + compiler?.close() + initialized = false + } - override fun connectClient(client: ILanguageClient?) { - this._client = client - } + override fun connectClient(client: ILanguageClient?) { + this._client = client + } override fun applySettings(settings: IServerSettings?) { - this._settings = settings - } + this._settings = settings + } - override fun setupWithProject(workspace: Workspace) { - log.info("setupWithProject called, initialized={}", initialized) - if (!initialized) { - initialized = true - } - } + override fun setupWithProject(workspace: Workspace) { + log.info("setupWithProject called, initialized={}", initialized) + if (!initialized) { + recreateSession(workspace) + initialized = true + } + } + private fun recreateSession(workspace: Workspace) { + compiler?.close() + + val jdkHome = Environment.JAVA_HOME.toPath() + val jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE + val intellijPluginRoot = Paths.get( + BaseApplication + .baseInstance.applicationInfo.sourceDir + ) + + val jdkPlatform = JvmPlatforms.jvmPlatformByTargetVersion( + JvmTarget.supportedValues().first { it.majorVersion == jdkRelease }) + + compiler = Compiler( + intellijPluginRoot = intellijPluginRoot, + jdkHome = jdkHome, + jdkRelease = jdkRelease, + languageVersion = LanguageVersion.LATEST_STABLE + ) { + buildKtModuleProvider { + platform = jdkPlatform + + val moduleProjects = + workspace.subProjects + .filterIsInstance() + .filter { it.path != workspace.rootProject.path } + + val libraryDependencies = + moduleProjects + .flatMap { it.getCompileClasspaths() } + .associateWith { library -> + addModule(buildKtLibraryModule { + addBinaryRoot(library.toPath()) + }) + } + + val subprojectsAsModules = mutableMapOf() + + fun getOrCreateModule(project: ModuleProject): KaSourceModule { + subprojectsAsModules[project]?.also { module -> + // a source module already exists for this project + return module + } + + val module = buildKtSourceModule { + addSourceRoots( + project.getSourceDirectories().map { it.toPath() }) + + project.getCompileClasspaths(excludeSourceGeneratedClassPath = true) + .forEach { classpath -> + val libDependency = libraryDependencies[classpath] + if (libDependency == null) { + log.error( + "Unable to locate library module for classpath: {}", + libDependency + ) + return@forEach + } + + addRegularDependency(libDependency) + } + + project.getCompileModuleProjects() + .forEach { dependencyModule -> + addRegularDependency(getOrCreateModule(dependencyModule)) + } + } + + subprojectsAsModules[project] = module + return module + } + + moduleProjects.forEach { project -> + addModule(getOrCreateModule(project)) + } + } + } + } - override fun complete(params: CompletionParams?): CompletionResult { - return CompletionResult.EMPTY - } + override fun complete(params: CompletionParams?): CompletionResult { + return CompletionResult.EMPTY + } - override suspend fun findReferences(params: ReferenceParams): ReferenceResult { - if (!settings.referencesEnabled()) { - return ReferenceResult.empty() - } + override suspend fun findReferences(params: ReferenceParams): ReferenceResult { + if (!settings.referencesEnabled()) { + return ReferenceResult.empty() + } - if (!DocumentUtils.isKotlinFile(params.file)) { - return ReferenceResult.empty() - } + if (!DocumentUtils.isKotlinFile(params.file)) { + return ReferenceResult.empty() + } - return ReferenceResult.empty() - } + return ReferenceResult.empty() + } - override suspend fun findDefinition(params: DefinitionParams): DefinitionResult { - if (!settings.definitionsEnabled()) { - return DefinitionResult.empty() - } + override suspend fun findDefinition(params: DefinitionParams): DefinitionResult { + if (!settings.definitionsEnabled()) { + return DefinitionResult.empty() + } - if (!DocumentUtils.isKotlinFile(params.file)) { - return DefinitionResult.empty() - } + if (!DocumentUtils.isKotlinFile(params.file)) { + return DefinitionResult.empty() + } return DefinitionResult.empty() - } + } - override suspend fun expandSelection(params: ExpandSelectionParams): Range { - return params.selection - } + override suspend fun expandSelection(params: ExpandSelectionParams): Range { + return params.selection + } - override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp { - if (!settings.signatureHelpEnabled()) { - return SignatureHelp.empty() - } + override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp { + if (!settings.signatureHelpEnabled()) { + return SignatureHelp.empty() + } - if (!DocumentUtils.isKotlinFile(params.file)) { - return SignatureHelp.empty() - } + if (!DocumentUtils.isKotlinFile(params.file)) { + return SignatureHelp.empty() + } return SignatureHelp.empty() - } - - override suspend fun analyze(file: Path): DiagnosticResult { - log.debug("analyze(file={})", file) - - if (!settings.diagnosticsEnabled() || !settings.codeAnalysisEnabled()) { - log.debug("analyze() skipped: diagnosticsEnabled={}, codeAnalysisEnabled={}", - settings.diagnosticsEnabled(), settings.codeAnalysisEnabled()) - return DiagnosticResult.NO_UPDATE - } - - if (!DocumentUtils.isKotlinFile(file)) { - log.debug("analyze() skipped: not a Kotlin file") - return DiagnosticResult.NO_UPDATE - } - - return DiagnosticResult.NO_UPDATE - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("unused") - fun onDocumentOpen(event: DocumentOpenEvent) { - if (!DocumentUtils.isKotlinFile(event.openedFile)) { - return - } - - selectedFile = event.openedFile - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("unused") - fun onDocumentChange(event: DocumentChangeEvent) { - if (!DocumentUtils.isKotlinFile(event.changedFile)) { - return - } - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("unused") - fun onDocumentClose(event: DocumentCloseEvent) { - if (!DocumentUtils.isKotlinFile(event.closedFile)) { - return - } - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("unused") - fun onDocumentSelected(event: DocumentSelectedEvent) { - if (!DocumentUtils.isKotlinFile(event.selectedFile)) { - return - } - - selectedFile = event.selectedFile - val uri = event.selectedFile.toUri().toString() - - log.debug("onDocumentSelected: uri={}", uri) - } + } + + override suspend fun analyze(file: Path): DiagnosticResult { + log.debug("analyze(file={})", file) + + if (!settings.diagnosticsEnabled() || !settings.codeAnalysisEnabled()) { + log.debug( + "analyze() skipped: diagnosticsEnabled={}, codeAnalysisEnabled={}", + settings.diagnosticsEnabled(), settings.codeAnalysisEnabled() + ) + return DiagnosticResult.NO_UPDATE + } + + if (!DocumentUtils.isKotlinFile(file)) { + log.debug("analyze() skipped: not a Kotlin file") + return DiagnosticResult.NO_UPDATE + } + + return DiagnosticResult.NO_UPDATE + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("unused") + fun onDocumentOpen(event: DocumentOpenEvent) { + if (!DocumentUtils.isKotlinFile(event.openedFile)) { + return + } + + selectedFile = event.openedFile + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("unused") + fun onDocumentChange(event: DocumentChangeEvent) { + if (!DocumentUtils.isKotlinFile(event.changedFile)) { + return + } + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("unused") + fun onDocumentClose(event: DocumentCloseEvent) { + if (!DocumentUtils.isKotlinFile(event.closedFile)) { + return + } + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("unused") + fun onDocumentSelected(event: DocumentSelectedEvent) { + if (!DocumentUtils.isKotlinFile(event.selectedFile)) { + return + } + + selectedFile = event.selectedFile + val uri = event.selectedFile.toUri().toString() + + log.debug("onDocumentSelected: uri={}", uri) + } } 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 new file mode 100644 index 0000000000..8414b2dc73 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt @@ -0,0 +1,75 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISessionBuilder +import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession +import org.jetbrains.kotlin.cli.common.intellijPluginRoot +import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer +import org.jetbrains.kotlin.config.ApiVersion +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.LanguageFeature +import org.jetbrains.kotlin.config.LanguageVersion +import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl +import org.jetbrains.kotlin.config.jdkHome +import org.jetbrains.kotlin.config.jdkRelease +import org.jetbrains.kotlin.config.languageVersionSettings +import org.jetbrains.kotlin.config.moduleName +import org.jetbrains.kotlin.config.useFir +import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil +import org.jetbrains.kotlin.psi.KtPsiFactory +import java.nio.file.Path +import kotlin.io.path.pathString + +/** + * A compilation environment for compiling Kotlin sources. + * + * @param intellijPluginRoot The IntelliJ plugin root. This is usually the location of the embeddable JAR file. Required. + * @param languageVersion The language version that this environment should be compatible with. + * @param jdkHome Path to the JDK installation directory. + * @param jdkRelease The JDK release version at [jdkHome]. + */ +class CompilationEnvironment( + intellijPluginRoot: Path, + jdkHome: Path, + jdkRelease: Int, + languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, + configureSession: StandaloneAnalysisAPISessionBuilder.() -> Unit = {} +) : AutoCloseable { + private val disposable = Disposer.newDisposable() + + val session: StandaloneAnalysisAPISession + val parser: KtPsiFactory + + init { + val configuration = CompilerConfiguration().apply { + this.moduleName = JvmProtoBufUtil.DEFAULT_MODULE_NAME + this.useFir = true + this.intellijPluginRoot = intellijPluginRoot.pathString + this.languageVersionSettings = LanguageVersionSettingsImpl( + languageVersion = languageVersion, + apiVersion = ApiVersion.createByLanguageVersion(languageVersion), + analysisFlags = emptyMap(), + specificFeatures = buildMap { + // enable all features + LanguageFeature.entries.associateWith { LanguageFeature.State.ENABLED } + } + ) + + this.jdkHome = jdkHome.toFile() + this.jdkRelease = jdkRelease + } + + session = buildStandaloneAnalysisAPISession( + projectDisposable = disposable, + unitTestMode = false, + compilerConfiguration = configuration, + init = configureSession + ) + + parser = KtPsiFactory(session.project) + } + + override fun close() { + disposable.dispose() + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt new file mode 100644 index 0000000000..a15305ffba --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt @@ -0,0 +1,16 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +/** + * The kind of compilation being performed in a [Compiler]. + */ +enum class CompilationKind { + /** + * The default compilation kind. Mostly used for normal Kotlin source files. + */ + Default, + + /** + * Compilation kind for compiling Kotlin scripts. + */ + Script, +} \ 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 new file mode 100644 index 0000000000..5baf9f6614 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt @@ -0,0 +1,79 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISessionBuilder +import org.jetbrains.kotlin.com.intellij.lang.Language +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.PsiFile +import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.config.LanguageVersion +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.jetbrains.kotlin.psi.KtFile +import org.slf4j.LoggerFactory +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.pathString + +class Compiler( + intellijPluginRoot: Path, + jdkHome: Path, + jdkRelease: Int, + languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, + configureSession: StandaloneAnalysisAPISessionBuilder.() -> Unit = {}, +) : AutoCloseable { + private val logger = LoggerFactory.getLogger(Compiler::class.java) + private val fileSystem = + VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) + + private val defaultCompilationEnv = CompilationEnvironment( + intellijPluginRoot, + jdkHome, + jdkRelease, + languageVersion, + configureSession, + ) + + fun compilationEnvironmentFor(compilationKind: CompilationKind): CompilationEnvironment = + when (compilationKind) { + CompilationKind.Default -> defaultCompilationEnv + CompilationKind.Script -> throw UnsupportedOperationException("Not supported yet") + } + + fun psiFileFactoryFor(compilationKind: CompilationKind): PsiFileFactory = + PsiFileFactory.getInstance(compilationEnvironmentFor(compilationKind).session.project) + + fun createPsiFileFor( + content: String, + file: Path = Paths.get("dummy.virtual.kt"), + language: Language = KotlinLanguage.INSTANCE, + compilationKind: CompilationKind = CompilationKind.Default + ): PsiFile { + require(!content.contains('\r')) + + val psiFile = psiFileFactoryFor(compilationKind).createFileFromText( + file.pathString, + language, + content, + true, + false + ) + check(psiFile.virtualFile != null) { + "No virtual-file associated with newly created psiFile" + } + + return psiFile + } + + fun createKtFile( + content: String, + file: Path = Paths.get("dummy.virtual.kt"), + compilationKind: CompilationKind = CompilationKind.Default + ): KtFile = + createPsiFileFor(content, file, KotlinLanguage.INSTANCE, compilationKind) as KtFile + + override fun close() { + defaultCompilationEnv.close() + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt new file mode 100644 index 0000000000..28e8dcdf9b --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt @@ -0,0 +1,10 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.config.LanguageVersion + +internal val DEFAULT_LANGUAGE_VERSION = + LanguageVersion.LATEST_STABLE + +internal val DEFAULT_JVM_TARGET = + JvmTarget.JVM_11 diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt index a91c1ab5e7..39a70b9cc6 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt @@ -83,7 +83,8 @@ open class AndroidModule( override fun getClassPaths(): Set = getModuleClasspaths() - fun getVariant(name: String): AndroidModels.AndroidVariant? = variantList.firstOrNull { it.name == name } + fun getVariant(name: String): AndroidModels.AndroidVariant? = + variantList.firstOrNull { it.name == name } fun getResourceDirectories(): Set { if (mainSourceSet == null) { @@ -140,14 +141,24 @@ open class AndroidModule( addAll(getSelectedVariant()?.mainArtifact?.classJars ?: emptyList()) } - override fun getCompileClasspaths(): Set { + override fun getCompileClasspaths(excludeSourceGeneratedClassPath: Boolean): Set { val project = IProjectManager.getInstance().workspace ?: return emptySet() val result = mutableSetOf() - result.addAll(getModuleClasspaths()) + if (excludeSourceGeneratedClassPath) { + // TODO: The mainArtifact.classJars are technically generated from source files + // But they're also kind-of not and are required for resolving R.* symbols + // Should we instead split this API into more fine-tuned getters? + result.addAll( + getSelectedVariant()?.mainArtifact?.classJars ?: emptyList() + ) + } else { + result.addAll(getModuleClasspaths()) + } collectLibraries( root = project, libraries = variantDependencies.mainArtifact?.compileDependencyList ?: emptyList(), result = result, + excludeSourceGeneratedClassPath = excludeSourceGeneratedClassPath, ) return result } @@ -169,7 +180,10 @@ open class AndroidModule( .forEach { result.add(it) } } - val rClassDir = File(buildDirectory, "intermediates/compile_and_runtime_not_namespaced_r_class_jar/$variant") + val rClassDir = File( + buildDirectory, + "intermediates/compile_and_runtime_not_namespaced_r_class_jar/$variant" + ) if (rClassDir.exists()) { rClassDir.walkTopDown() .filter { it.name == "R.jar" && it.isFile } @@ -184,7 +198,11 @@ open class AndroidModule( val variant = getSelectedVariant()?.name ?: "debug" val buildDirectory = delegate.buildDir - log.info("getRuntimeDexFiles: buildDir={}, variant={}", buildDirectory.absolutePath, variant) + log.info( + "getRuntimeDexFiles: buildDir={}, variant={}", + buildDirectory.absolutePath, + variant + ) val dexDir = File(buildDirectory, "intermediates/dex/$variant") log.info(" Checking dexDir: {} (exists: {})", dexDir.absolutePath, dexDir.exists()) @@ -198,7 +216,11 @@ open class AndroidModule( } val mergeProjectDexDir = File(buildDirectory, "intermediates/project_dex_archive/$variant") - log.info(" Checking project_dex_archive: {} (exists: {})", mergeProjectDexDir.absolutePath, mergeProjectDexDir.exists()) + log.info( + " Checking project_dex_archive: {} (exists: {})", + mergeProjectDexDir.absolutePath, + mergeProjectDexDir.exists() + ) if (mergeProjectDexDir.exists()) { mergeProjectDexDir.walkTopDown() .filter { it.name.endsWith(".dex") && it.isFile } @@ -216,6 +238,7 @@ open class AndroidModule( root: Workspace, libraries: List, result: MutableSet, + excludeSourceGeneratedClassPath: Boolean = false, ) { val libraryMap = variantDependencies.librariesMap for (library in libraries) { @@ -226,7 +249,7 @@ open class AndroidModule( continue } - result.addAll(module.getCompileClasspaths()) + result.addAll(module.getCompileClasspaths(excludeSourceGeneratedClassPath)) } else if (lib.type == AndroidModels.LibraryType.ExternalAndroidLibrary && lib.hasAndroidLibraryData()) { result.addAll(lib.androidLibraryData.compileJarFiles) } else if (lib.type == AndroidModels.LibraryType.ExternalJavaLibrary && lib.hasArtifactPath()) { @@ -386,7 +409,8 @@ open class AndroidModule( var deps: Int val androidLibraries = variantDependencies.librariesMap.values.mapNotNull { library -> - val packageName = library.androidLibraryData?.findPackageName() ?: UNKNOWN_PACKAGE + val packageName = + library.androidLibraryData?.findPackageName() ?: UNKNOWN_PACKAGE if (library.type != AndroidModels.LibraryType.ExternalAndroidLibrary || !library.hasAndroidLibraryData() || !library.androidLibraryData!!.resFolder.exists() || @@ -563,5 +587,6 @@ open class AndroidModule( return variant } - private fun getPlatformDir() = bootClassPaths.firstOrNull { it.name == "android.jar" }?.parentFile + private fun getPlatformDir() = + bootClassPaths.firstOrNull { it.name == "android.jar" }?.parentFile } diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/JavaModule.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/JavaModule.kt index 95cca3a7a7..e24dc9079a 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/JavaModule.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/JavaModule.kt @@ -86,9 +86,18 @@ class JavaModule( override fun getModuleClasspaths(): Set = mutableSetOf(classesJar) - override fun getCompileClasspaths(): Set { - val classpaths = getModuleClasspaths().toMutableSet() - getCompileModuleProjects().forEach { classpaths.addAll(it.getCompileClasspaths()) } + override fun getCompileClasspaths(excludeSourceGeneratedClassPath: Boolean): Set { + val classpaths = + if (excludeSourceGeneratedClassPath) mutableSetOf() else getModuleClasspaths().toMutableSet() + + getCompileModuleProjects().forEach { + classpaths.addAll( + it.getCompileClasspaths( + excludeSourceGeneratedClassPath + ) + ) + } + classpaths.addAll(getDependencyClassPaths()) return classpaths } @@ -126,9 +135,9 @@ class JavaModule( ): Boolean = this.dependencyList.any { dependency -> dependency.hasExternalLibrary() && - dependency.externalLibrary.libraryInfo?.let { artifact -> - artifact.group == group && artifact.name == name - } ?: false + dependency.externalLibrary.libraryInfo?.let { artifact -> + artifact.group == group && artifact.name == name + } ?: false } fun getDependencyClassPaths(): Set = diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/ModuleProject.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/ModuleProject.kt index de2b35e503..5f45acc408 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/ModuleProject.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/ModuleProject.kt @@ -98,9 +98,12 @@ abstract class ModuleProject( * Get the classpaths with compile scope. This must include classpaths of transitive project * dependencies as well. This includes classpaths for this module as well. * + * @param excludeSourceGeneratedClassPath Whether to exclude classpath that's generated from + * source files of this module or its dependencies. Defaults to `false`. * @return The source directories. */ - abstract fun getCompileClasspaths(): Set + abstract fun getCompileClasspaths(excludeSourceGeneratedClassPath: Boolean): Set + fun getCompileClasspaths() = getCompileClasspaths(false) /** * Get the intermediate build output classpaths for this module. From bf7acd1032ea5e65013751788355d5ce2c75a133 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 24 Mar 2026 21:31:46 +0530 Subject: [PATCH 09/58] fix: JvmTarget resolution fails for input "21" Signed-off-by: Akash Yadav --- .../java/providers/JavaDiagnosticProvider.kt | 232 +++++++++--------- .../lsp/kotlin/KotlinLanguageServer.kt | 8 +- .../projects/models/ActiveDocument.kt | 24 +- 3 files changed, 133 insertions(+), 131 deletions(-) diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt index 8448d4401e..78671f4d67 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt @@ -37,120 +37,120 @@ import java.util.concurrent.atomic.AtomicBoolean */ class JavaDiagnosticProvider { - private val analyzeTimestamps = mutableMapOf() - private var cachedDiagnostics = DiagnosticResult.NO_UPDATE - private var analyzing = AtomicBoolean(false) - private var analyzingThread: AnalyzingThread? = null - - companion object { - - private val log = LoggerFactory.getLogger(JavaDiagnosticProvider::class.java) - } - - fun analyze(file: Path): DiagnosticResult { - - val module = IProjectManager.getInstance().findModuleForFile(file, false) - ?: return DiagnosticResult.NO_UPDATE - val compiler = JavaCompilerService(module) - - abortIfCancelled() - - log.debug("Analyzing: {}", file) - - val modifiedAt = FileManager.getLastModified(file) - val analyzedAt = analyzeTimestamps[file] - - if (analyzedAt?.isAfter(modifiedAt) == true) { - log.debug("Using cached analyze results...") - return cachedDiagnostics - } - - analyzingThread?.let { analyzingThread -> - if (analyzing.get()) { - log.debug("Cancelling currently analyzing thread...") - ProgressManager.instance.cancel(analyzingThread) - this.analyzingThread = null - } - } - - analyzing.set(true) - - val analyzingThread = AnalyzingThread(compiler, file).also { - analyzingThread = it - it.start() - it.join() - } - - return analyzingThread.result.also { - this.analyzingThread = null - } - } - - fun isAnalyzing(): Boolean { - return this.analyzing.get() - } - - fun cancel() { - this.analyzingThread?.cancel() - } - - fun clearTimestamp(file: Path) { - analyzeTimestamps.remove(file) - } - - private fun doAnalyze(file: Path, task: CompileTask): DiagnosticResult { - val result = - if (!isTaskValid(task)) { - // Do not use Collections.emptyList () - // The returned list is accessed and the list returned by Collections.emptyList() - // throws exception when trying to access. - log.info("Using cached diagnostics") - cachedDiagnostics - } else - DiagnosticResult( - file, - findDiagnostics(task, file).sortedBy { - it.range - } - ) - return result.also { - log.info("Analyze file completed. Found {} diagnostic items", result.diagnostics.size) - } - } - - private fun isTaskValid(task: CompileTask?): Boolean { - abortIfCancelled() - return task?.task != null && task.roots != null && task.roots.size > 0 - } - - inner class AnalyzingThread(val compiler: JavaCompilerService, val file: Path) : - Thread("JavaAnalyzerThread") { - - var result: DiagnosticResult = DiagnosticResult.NO_UPDATE - - fun cancel() { - ProgressManager.instance.cancel(this) - } - - override fun run() { - result = - try { - compiler.compile(file).get { task -> doAnalyze(file, task) } - } catch (err: Throwable) { - if (CancelChecker.isCancelled(err)) { - log.error("Analyze request cancelled") - } else { - log.warn("Unable to analyze file", err) - } - DiagnosticResult.NO_UPDATE - } finally { - compiler.destroy() - analyzing.set(false) - } - .also { - cachedDiagnostics = it - analyzeTimestamps[file] = Instant.now() - } - } - } + private val analyzeTimestamps = mutableMapOf() + private var cachedDiagnostics = DiagnosticResult.NO_UPDATE + private var analyzing = AtomicBoolean(false) + private var analyzingThread: AnalyzingThread? = null + + companion object { + + private val log = LoggerFactory.getLogger(JavaDiagnosticProvider::class.java) + } + + fun analyze(file: Path): DiagnosticResult { + + val module = IProjectManager.getInstance().findModuleForFile(file, false) + ?: return DiagnosticResult.NO_UPDATE + val compiler = JavaCompilerService(module) + + abortIfCancelled() + + log.debug("Analyzing: {}", file) + + val modifiedAt = FileManager.getLastModified(file) + val analyzedAt = analyzeTimestamps[file] + + if (analyzedAt?.isAfter(modifiedAt) == true) { + log.debug("Using cached analyze results...") + return cachedDiagnostics + } + + analyzingThread?.let { analyzingThread -> + if (analyzing.get()) { + log.debug("Cancelling currently analyzing thread...") + ProgressManager.instance.cancel(analyzingThread) + this.analyzingThread = null + } + } + + analyzing.set(true) + + val analyzingThread = AnalyzingThread(compiler, file).also { + analyzingThread = it + it.start() + it.join() + } + + return analyzingThread.result.also { + this.analyzingThread = null + } + } + + fun isAnalyzing(): Boolean { + return this.analyzing.get() + } + + fun cancel() { + this.analyzingThread?.cancel() + } + + fun clearTimestamp(file: Path) { + analyzeTimestamps.remove(file) + } + + private fun doAnalyze(file: Path, task: CompileTask): DiagnosticResult { + val result = + if (!isTaskValid(task)) { + // Do not use Collections.emptyList () + // The returned list is accessed and the list returned by Collections.emptyList() + // throws exception when trying to access. + log.info("Using cached diagnostics") + cachedDiagnostics + } else + DiagnosticResult( + file, + findDiagnostics(task, file).sortedBy { + it.range + } + ) + return result.also { + log.info("Analyze file completed. Found {} diagnostic items", result.diagnostics.size) + } + } + + private fun isTaskValid(task: CompileTask?): Boolean { + abortIfCancelled() + return task?.task != null && task.roots != null && task.roots.size > 0 + } + + inner class AnalyzingThread(val compiler: JavaCompilerService, val file: Path) : + Thread("JavaAnalyzerThread") { + + var result: DiagnosticResult = DiagnosticResult.NO_UPDATE + + fun cancel() { + ProgressManager.instance.cancel(this) + } + + override fun run() { + result = + try { + compiler.compile(file).get { task -> doAnalyze(file, task) } + } catch (err: Throwable) { + if (CancelChecker.isCancelled(err)) { + log.error("Analyze request cancelled") + } else { + log.warn("Unable to analyze file", err) + } + DiagnosticResult.NO_UPDATE + } finally { + compiler.destroy() + analyzing.set(false) + } + .also { + cachedDiagnostics = it + analyzeTimestamps[file] = Instant.now() + } + } + } } 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 bf9275787f..149e00fc09 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 @@ -119,8 +119,10 @@ class KotlinLanguageServer : ILanguageServer { .baseInstance.applicationInfo.sourceDir ) - val jdkPlatform = JvmPlatforms.jvmPlatformByTargetVersion( - JvmTarget.supportedValues().first { it.majorVersion == jdkRelease }) + val jvmTarget = JvmTarget.fromString(IJdkDistributionProvider.DEFAULT_JAVA_VERSION) + ?: JvmTarget.JVM_21 + + val jvmPlatform = JvmPlatforms.jvmPlatformByTargetVersion(jvmTarget) compiler = Compiler( intellijPluginRoot = intellijPluginRoot, @@ -129,7 +131,7 @@ class KotlinLanguageServer : ILanguageServer { languageVersion = LanguageVersion.LATEST_STABLE ) { buildKtModuleProvider { - platform = jdkPlatform + platform = jvmPlatform val moduleProjects = workspace.subProjects diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveDocument.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveDocument.kt index 5f23eeb68a..42b0b7e6cc 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveDocument.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/models/ActiveDocument.kt @@ -28,20 +28,20 @@ import java.time.Instant * @author Akash Yadav */ open class ActiveDocument( - val file: Path, - var version: Int, - var modified: Instant, - content: String = "" + val file: Path, + var version: Int, + var modified: Instant, + content: String = "" ) { - var content: String = content - internal set + var content: String = content + internal set - fun inputStream(): BufferedInputStream { - return content.byteInputStream().buffered() - } + fun inputStream(): BufferedInputStream { + return content.byteInputStream().buffered() + } - fun reader(): BufferedReader { - return content.reader().buffered() - } + fun reader(): BufferedReader { + return content.reader().buffered() + } } From dc62a51d5feb9625e0676dd99d6c506db8bf2fbb Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 24 Mar 2026 21:32:28 +0530 Subject: [PATCH 10/58] fix: do not early-init VirtualFileSystem Signed-off-by: Akash Yadav --- .../androidide/lsp/kotlin/compiler/Compiler.kt | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) 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 5baf9f6614..2bbaf4bbee 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 @@ -2,14 +2,10 @@ package com.itsaky.androidide.lsp.kotlin.compiler import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISessionBuilder import org.jetbrains.kotlin.com.intellij.lang.Language -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.PsiFile import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory -import org.jetbrains.kotlin.config.JvmTarget import org.jetbrains.kotlin.config.LanguageVersion import org.jetbrains.kotlin.idea.KotlinLanguage -import org.jetbrains.kotlin.platform.jvm.JvmPlatforms import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory import java.nio.file.Path @@ -24,15 +20,13 @@ class Compiler( configureSession: StandaloneAnalysisAPISessionBuilder.() -> Unit = {}, ) : AutoCloseable { private val logger = LoggerFactory.getLogger(Compiler::class.java) - private val fileSystem = - VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) private val defaultCompilationEnv = CompilationEnvironment( - intellijPluginRoot, - jdkHome, - jdkRelease, - languageVersion, - configureSession, + intellijPluginRoot = intellijPluginRoot, + jdkHome = jdkHome, + jdkRelease = jdkRelease, + languageVersion = languageVersion, + configureSession = configureSession, ) fun compilationEnvironmentFor(compilationKind: CompilationKind): CompilationEnvironment = From 4b1c8e4e0d2769bb4116ed9ca98c9346512fa8f3 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 25 Mar 2026 16:28:41 +0530 Subject: [PATCH 11/58] fix: remove replaceClass desugar instruction for Unsafe Signed-off-by: Akash Yadav --- app/build.gradle.kts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 554718fb66..d9dcb121a5 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -205,18 +205,6 @@ desugaring { EnvUtil::logbackVersion.javaMethod!!, DesugarEnvUtil::logbackVersion.javaMethod!!, ) - - // Replace usages of Unsafe class (from com.intellij.util.containers) - // with our own implementation - // The original implementation uses MethodHandle instances to access APIs - // from sun.misc.Unsafe which are not directly accessible on Android - // As a result, we have our implementatio of that class which makes use - // of HiddenApiBypass to access the same methods, and provides a drop-in - // replacement of the original class - replaceClass( - "org.jetbrains.kotlin.com.intellij.util.containers.Unsafe", - "org.jetbrains.kotlin.com.intellij.util.containers.UnsafeImpl", - ) } } From b14f6ee0aa096dbf5b495c1f603d9f4acc7c73d9 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 26 Mar 2026 16:35:23 +0530 Subject: [PATCH 12/58] fix: ensure boot class path is added as dependency to Android modules Signed-off-by: Akash Yadav --- .../lsp/kotlin/KotlinLanguageServer.kt | 27 +++++++++++++- .../kotlin/compiler/CompilationEnvironment.kt | 37 ++++++++++++++++++- .../lsp/kotlin/compiler/Compiler.kt | 26 +++++++++---- 3 files changed, 80 insertions(+), 10 deletions(-) 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 149e00fc09..40c52ba404 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 @@ -38,8 +38,10 @@ import com.itsaky.androidide.lsp.models.ReferenceResult import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams import com.itsaky.androidide.models.Range +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 com.itsaky.androidide.utils.DocumentUtils import com.itsaky.androidide.utils.Environment import org.greenrobot.eventbus.EventBus @@ -50,12 +52,10 @@ import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryMod import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule import org.jetbrains.kotlin.config.JvmTarget import org.jetbrains.kotlin.config.LanguageVersion -import org.jetbrains.kotlin.platform.jvm.JdkPlatform import org.jetbrains.kotlin.platform.jvm.JvmPlatforms import org.slf4j.LoggerFactory import java.nio.file.Path import java.nio.file.Paths -import kotlin.io.path.pathString class KotlinLanguageServer : ILanguageServer { @@ -138,11 +138,27 @@ class KotlinLanguageServer : ILanguageServer { .filterIsInstance() .filter { it.path != workspace.rootProject.path } + val bootClassPaths = + moduleProjects + .filterIsInstance() + .flatMap { project -> + project.bootClassPaths + .map { bootClassPath -> + addModule(buildKtLibraryModule { + this.platform = jvmPlatform + this.libraryName = bootClassPath.nameWithoutExtension + addBinaryRoot(bootClassPath.toPath()) + }) + } + } + val libraryDependencies = moduleProjects .flatMap { it.getCompileClasspaths() } .associateWith { library -> addModule(buildKtLibraryModule { + this.platform = jvmPlatform + this.libraryName = library.nameWithoutExtension addBinaryRoot(library.toPath()) }) } @@ -156,9 +172,16 @@ class KotlinLanguageServer : ILanguageServer { } val module = buildKtSourceModule { + this.platform = jvmPlatform + this.moduleName = project.name addSourceRoots( project.getSourceDirectories().map { it.toPath() }) + // always dependent on boot class paths, if any + bootClassPaths.forEach { bootClassPathModule -> + addRegularDependency(bootClassPathModule) + } + project.getCompileClasspaths(excludeSourceGeneratedClassPath = true) .forEach { classpath -> val libDependency = libraryDependencies[classpath] 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 8414b2dc73..4c8a84d2f5 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 @@ -4,7 +4,12 @@ import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISessionBuilder import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession 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.com.intellij.openapi.util.Disposer +import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager +import org.jetbrains.kotlin.com.intellij.psi.PsiManager import org.jetbrains.kotlin.config.ApiVersion import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.LanguageFeature @@ -13,10 +18,12 @@ import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl import org.jetbrains.kotlin.config.jdkHome import org.jetbrains.kotlin.config.jdkRelease 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.metadata.jvm.deserialization.JvmProtoBufUtil import org.jetbrains.kotlin.psi.KtPsiFactory +import org.slf4j.LoggerFactory import java.nio.file.Path import kotlin.io.path.pathString @@ -39,6 +46,30 @@ class CompilationEnvironment( val session: StandaloneAnalysisAPISession val parser: KtPsiFactory + val psiManager: PsiManager + val psiDocumentManager: PsiDocumentManager + + private val envMessageCollector = object: MessageCollector { + override fun clear() { + } + + override fun report( + severity: CompilerMessageSeverity, + message: String, + location: CompilerMessageSourceLocation? + ) { + logger.info("[{}] {} ({})", severity.name, message, location) + } + + override fun hasErrors(): Boolean { + return false + } + + } + + companion object { + private val logger = LoggerFactory.getLogger(CompilationEnvironment::class.java) + } init { val configuration = CompilerConfiguration().apply { @@ -51,12 +82,14 @@ class CompilationEnvironment( analysisFlags = emptyMap(), specificFeatures = buildMap { // enable all features - LanguageFeature.entries.associateWith { LanguageFeature.State.ENABLED } + putAll(LanguageFeature.entries.associateWith { LanguageFeature.State.ENABLED }) } ) this.jdkHome = jdkHome.toFile() this.jdkRelease = jdkRelease + + this.messageCollector = envMessageCollector } session = buildStandaloneAnalysisAPISession( @@ -67,6 +100,8 @@ class CompilationEnvironment( ) parser = KtPsiFactory(session.project) + psiManager = PsiManager.getInstance(session.project) + psiDocumentManager = PsiDocumentManager.getInstance(session.project) } override fun close() { 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 2bbaf4bbee..9dc0ff24b5 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 @@ -2,6 +2,9 @@ package com.itsaky.androidide.lsp.kotlin.compiler import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISessionBuilder import org.jetbrains.kotlin.com.intellij.lang.Language +import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileSystem import org.jetbrains.kotlin.com.intellij.psi.PsiFile import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory import org.jetbrains.kotlin.config.LanguageVersion @@ -20,14 +23,23 @@ class Compiler( configureSession: StandaloneAnalysisAPISessionBuilder.() -> Unit = {}, ) : AutoCloseable { private val logger = LoggerFactory.getLogger(Compiler::class.java) + private val defaultCompilationEnv: CompilationEnvironment - private val defaultCompilationEnv = CompilationEnvironment( - intellijPluginRoot = intellijPluginRoot, - jdkHome = jdkHome, - jdkRelease = jdkRelease, - languageVersion = languageVersion, - configureSession = configureSession, - ) + val fileSystem: VirtualFileSystem + + init { + defaultCompilationEnv = CompilationEnvironment( + intellijPluginRoot = intellijPluginRoot, + jdkHome = jdkHome, + jdkRelease = jdkRelease, + languageVersion = languageVersion, + configureSession = configureSession, + ) + + // must be initialized AFTER the compilation env has been initialized + fileSystem = VirtualFileManager.getInstance() + .getFileSystem(StandardFileSystems.FILE_PROTOCOL) + } fun compilationEnvironmentFor(compilationKind: CompilationKind): CompilationEnvironment = when (compilationKind) { From 54ca7a94d5a79cac780530d5a10598c2e57b31f7 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 26 Mar 2026 16:35:53 +0530 Subject: [PATCH 13/58] feat: add diagnostic provider for Kotlin Signed-off-by: Akash Yadav --- .../lsp/kotlin/KotlinLanguageServer.kt | 58 ++++++- .../diagnostic/KotlinDiagnosticProvider.kt | 154 ++++++++++++++++++ 2 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt 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 40c52ba404..eb0546f19d 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 @@ -27,6 +27,7 @@ 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.diagnostic.KotlinDiagnosticProvider import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.DefinitionParams @@ -38,12 +39,22 @@ import com.itsaky.androidide.lsp.models.ReferenceResult import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams import com.itsaky.androidide.models.Range +import com.itsaky.androidide.projects.FileManager 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 com.itsaky.androidide.utils.DocumentUtils import com.itsaky.androidide.utils.Environment +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.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -54,8 +65,10 @@ 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 { @@ -64,7 +77,11 @@ class KotlinLanguageServer : ILanguageServer { private var selectedFile: Path? = null private var initialized = false + private val scope = + CoroutineScope(SupervisorJob() + CoroutineName(KotlinLanguageServer::class.simpleName!!)) private var compiler: Compiler? = null + private var diagnosticProvider: KotlinDiagnosticProvider? = null + private var analyzeJob: Job? = null override val serverId: String = SERVER_ID @@ -75,6 +92,9 @@ 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 log = LoggerFactory.getLogger(KotlinLanguageServer::class.java) } @@ -89,6 +109,7 @@ class KotlinLanguageServer : ILanguageServer { override fun shutdown() { EventBus.getDefault().unregister(this) + scope.cancel("LSP is being shut down") compiler?.close() initialized = false } @@ -110,6 +131,7 @@ class KotlinLanguageServer : ILanguageServer { } private fun recreateSession(workspace: Workspace) { + diagnosticProvider?.close() compiler?.close() val jdkHome = Environment.JAVA_HOME.toPath() @@ -211,6 +233,11 @@ class KotlinLanguageServer : ILanguageServer { } } } + + diagnosticProvider = KotlinDiagnosticProvider( + compiler = compiler!!, + scope = scope, + ) } override fun complete(params: CompletionParams?): CompletionResult { @@ -273,7 +300,8 @@ class KotlinLanguageServer : ILanguageServer { return DiagnosticResult.NO_UPDATE } - return DiagnosticResult.NO_UPDATE + return diagnosticProvider?.analyze(file) + ?: DiagnosticResult.NO_UPDATE } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -284,6 +312,27 @@ class KotlinLanguageServer : ILanguageServer { } 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) + } } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -292,6 +341,7 @@ class KotlinLanguageServer : ILanguageServer { if (!DocumentUtils.isKotlinFile(event.changedFile)) { return } + debouncingAnalyze() } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -300,6 +350,12 @@ class KotlinLanguageServer : ILanguageServer { if (!DocumentUtils.isKotlinFile(event.closedFile)) { return } + + diagnosticProvider?.clearTimestamp(event.closedFile) + if (FileManager.getActiveDocumentCount() == 0) { + selectedFile = null + analyzeJob?.cancel("No active files") + } } @Subscribe(threadMode = ThreadMode.ASYNC) 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 new file mode 100644 index 0000000000..37e34d03af --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt @@ -0,0 +1,154 @@ +package com.itsaky.androidide.lsp.kotlin.diagnostic + +import com.itsaky.androidide.lsp.kotlin.compiler.CompilationKind +import com.itsaky.androidide.lsp.kotlin.compiler.Compiler +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 com.itsaky.androidide.tasks.cancelIfActive +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +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.application.ApplicationManager +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.jetbrains.kotlin.com.intellij.testFramework.LightVirtualFile +import org.jetbrains.kotlin.psi.KtFile +import org.slf4j.LoggerFactory +import java.nio.file.Path +import java.time.Instant +import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.pathString +import org.jetbrains.kotlin.analysis.api.analyze as ktAnalyze + +class KotlinDiagnosticProvider( + private val compiler: Compiler, + private val scope: CoroutineScope +) : AutoCloseable { + + companion object { + private val logger = LoggerFactory.getLogger(KotlinDiagnosticProvider::class.java) + } + + private val analyzeTimestamps = ConcurrentHashMap() + + fun analyze(file: Path): DiagnosticResult = + try { + logger.info("Analyzing file: {}", file) + return doAnalyze(file) + } catch (err: Throwable) { + if (err is CancellationException) { + logger.debug("analysis cancelled") + throw err + } + logger.error("An error occurred analyzing file: {}", file, err) + return DiagnosticResult.NO_UPDATE + } + + @OptIn(KaExperimentalApi::class) + private fun doAnalyze(file: Path): DiagnosticResult { + val modifiedAt = FileManager.getLastModified(file) + val analyzedAt = analyzeTimestamps[file] + if (analyzedAt?.isAfter(modifiedAt) == true) { + logger.debug("Skipping analysis. File unmodified.") + return DiagnosticResult.NO_UPDATE + } + + logger.info("fetch document contents") + val fileContents = FileManager.getDocumentContents(file) + .replace("\r", "") + + val env = compiler.compilationEnvironmentFor(CompilationKind.Default) + val virtualFile = compiler.fileSystem.refreshAndFindFileByPath(file.pathString) + if (virtualFile == null) { + logger.warn("Unable to find virtual file for path: {}", file.pathString) + return DiagnosticResult.NO_UPDATE + } + + val ktFile = env.psiManager.findFile(virtualFile) + if (ktFile == null) { + logger.warn("Unable to find KtFile for path: {}", file.pathString) + return DiagnosticResult.NO_UPDATE + } + + if (ktFile !is KtFile) { + logger.warn("Expected KtFile, but found {} for path:{}", ktFile.javaClass, file.pathString) + return DiagnosticResult.NO_UPDATE + } + + val inMemoryPsi = compiler.createKtFile(fileContents, file, CompilationKind.Default) + inMemoryPsi.originalFile = ktFile + + val rawDiagnostics = ktAnalyze(inMemoryPsi) { + logger.info("ktFile.text={}", inMemoryPsi.text) + ktFile.collectDiagnostics(filter = KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) + } + + logger.info("Found {} diagnostics", rawDiagnostics.size) + + return DiagnosticResult( + file = file, + diagnostics = rawDiagnostics.map { rawDiagnostic -> + rawDiagnostic.toDiagnosticItem() + } + ).also { + analyzeTimestamps[file] = Instant.now() + } + } + + internal fun clearTimestamp(file: Path) { + analyzeTimestamps.remove(file) + } + + override fun close() { + scope.cancelIfActive("diagnostic provider is being destroyed") + } +} + +private fun KaDiagnosticWithPsi<*>.toDiagnosticItem(): DiagnosticItem { + val range = psi.textRange.toRange(psi.containingFile) + val severity = severity.toDiagnosticSeverity() + return DiagnosticItem( + message = defaultMessage, + code = "", + range = range, + source = "Kotlin", + severity = severity, + ) +} + +private fun KaSeverity.toDiagnosticSeverity(): DiagnosticSeverity { + return when (this) { + KaSeverity.ERROR -> DiagnosticSeverity.ERROR + KaSeverity.WARNING -> DiagnosticSeverity.WARNING + KaSeverity.INFO -> DiagnosticSeverity.INFO + } +} + +private fun TextRange.toRange(containingFile: PsiFile): Range { + val doc = PsiDocumentManager.getInstance(containingFile.project) + .getDocument(containingFile) ?: return Range.NONE + val startLine = doc.getLineNumber(startOffset) + val startCol = startOffset - doc.getLineStartOffset(startLine) + val endLine = doc.getLineNumber(endOffset) + val endCol = endOffset - doc.getLineStartOffset(endLine) + return Range( + start = Position( + line = startLine, + column = startCol, + index = startOffset, + ), + end = Position( + line = endLine, + column = endCol, + index = endOffset, + ) + ) +} From 6e4d4581e7c5fda8998ca628239de2c7ccebf16f Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 26 Mar 2026 16:42:08 +0530 Subject: [PATCH 14/58] fix: remove unnecessary log statement Signed-off-by: Akash Yadav --- .../androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt | 1 - 1 file changed, 1 deletion(-) 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 37e34d03af..73be5b1484 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 @@ -87,7 +87,6 @@ class KotlinDiagnosticProvider( inMemoryPsi.originalFile = ktFile val rawDiagnostics = ktAnalyze(inMemoryPsi) { - logger.info("ktFile.text={}", inMemoryPsi.text) ktFile.collectDiagnostics(filter = KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) } From 5d841a321fc186bf5bad453fd16c15ef8ecaf809 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 26 Mar 2026 19:46:21 +0530 Subject: [PATCH 15/58] fix: update to latest kotlin-android release Signed-off-by: Akash Yadav --- subprojects/kotlin-analysis-api/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subprojects/kotlin-analysis-api/build.gradle.kts b/subprojects/kotlin-analysis-api/build.gradle.kts index 57fe554dae..2e4f08710f 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}-a98fda0" +val ktAndroidTag = "v${ktAndroidVersion}-f047b07" 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 = "804781ae6c6cdbc5af1ca9a08959af9552395d48704a6c5fcb43b5516cb3e378", + sha256Checksum = "c9897c94ae1431fadeb4fa5b05dd4d478a60c4589f38f801e07c72405a7b34b1", ) } } From 4b7b0f2824a896f33a2aa6134cc6cd2f73510f07 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 26 Mar 2026 19:47:42 +0530 Subject: [PATCH 16/58] fix: always re-initialize K2 session on setupWithProject Signed-off-by: Akash Yadav --- .../itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 eb0546f19d..d8f45ab761 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 @@ -124,10 +124,8 @@ class KotlinLanguageServer : ILanguageServer { override fun setupWithProject(workspace: Workspace) { log.info("setupWithProject called, initialized={}", initialized) - if (!initialized) { - recreateSession(workspace) - initialized = true - } + recreateSession(workspace) + initialized = true } private fun recreateSession(workspace: Workspace) { From e2f137abc603cf112466b02283b0dd0f7db40a3b Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 26 Mar 2026 19:48:10 +0530 Subject: [PATCH 17/58] fix: diagnostics are always collected from the on-disk file Signed-off-by: Akash Yadav --- .../kotlin/compiler/CompilationEnvironment.kt | 3 ++- .../lsp/kotlin/compiler/Compiler.kt | 5 +++++ .../diagnostic/KotlinDiagnosticProvider.kt | 21 +++++++++++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) 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 4c8a84d2f5..f49753a39f 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 @@ -40,6 +40,7 @@ class CompilationEnvironment( jdkHome: Path, jdkRelease: Int, languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, + enableParserEventSystem: Boolean = true, configureSession: StandaloneAnalysisAPISessionBuilder.() -> Unit = {} ) : AutoCloseable { private val disposable = Disposer.newDisposable() @@ -99,7 +100,7 @@ class CompilationEnvironment( init = configureSession ) - parser = KtPsiFactory(session.project) + parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) psiManager = PsiManager.getInstance(session.project) psiDocumentManager = PsiDocumentManager.getInstance(session.project) } 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 9dc0ff24b5..263f554e02 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 @@ -10,6 +10,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory import org.jetbrains.kotlin.config.LanguageVersion import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtPsiFactory import org.slf4j.LoggerFactory import java.nio.file.Path import java.nio.file.Paths @@ -27,12 +28,16 @@ class Compiler( val fileSystem: VirtualFileSystem + val defaultKotlinParser: KtPsiFactory + get() = defaultCompilationEnv.parser + init { defaultCompilationEnv = CompilationEnvironment( intellijPluginRoot = intellijPluginRoot, jdkHome = jdkHome, jdkRelease = jdkRelease, languageVersion = languageVersion, + enableParserEventSystem = true, configureSession = configureSession, ) 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 73be5b1484..9b984adaaa 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 @@ -12,9 +12,12 @@ import com.itsaky.androidide.tasks.cancelIfActive import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.analyzeCopy 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.analysis.api.projectStructure.KaDanglingFileModule +import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode import org.jetbrains.kotlin.com.intellij.openapi.application.ApplicationManager import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager @@ -25,6 +28,7 @@ import org.slf4j.LoggerFactory import java.nio.file.Path import java.time.Instant import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.name import kotlin.io.path.pathString import org.jetbrains.kotlin.analysis.api.analyze as ktAnalyze @@ -79,15 +83,24 @@ class KotlinDiagnosticProvider( } if (ktFile !is KtFile) { - logger.warn("Expected KtFile, but found {} for path:{}", ktFile.javaClass, file.pathString) + logger.warn( + "Expected KtFile, but found {} for path:{}", + ktFile.javaClass, + file.pathString + ) return DiagnosticResult.NO_UPDATE } - val inMemoryPsi = compiler.createKtFile(fileContents, file, CompilationKind.Default) + val inMemoryPsi = compiler.defaultKotlinParser + .createFile(file.name, fileContents) inMemoryPsi.originalFile = ktFile - val rawDiagnostics = ktAnalyze(inMemoryPsi) { - ktFile.collectDiagnostics(filter = KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) + val rawDiagnostics = analyzeCopy( + useSiteElement = inMemoryPsi, + resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, + ) { + logger.info("ktFile.text={}", inMemoryPsi.text) + inMemoryPsi.collectDiagnostics(filter = KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) } logger.info("Found {} diagnostics", rawDiagnostics.size) From 09fc9ea123fcfe7f168b128c08979dfe02accdcb Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Mon, 30 Mar 2026 19:35:07 +0530 Subject: [PATCH 18/58] feat: add the ability to incrementally invalidate source roots on project re-sync Signed-off-by: Akash Yadav --- .../lsp/kotlin/KotlinLanguageServer.kt | 141 +++---------- .../kotlin/compiler/CompilationEnvironment.kt | 196 +++++++++++++++--- .../lsp/kotlin/compiler/Compiler.kt | 6 +- .../IncrementalModificationTracker.kt | 22 ++ .../lsp/kotlin/compiler/KotlinProjectModel.kt | 166 +++++++++++++++ .../diagnostic/KotlinDiagnosticProvider.kt | 5 + 6 files changed, 392 insertions(+), 144 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt 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 d8f45ab761..da9a96664a 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 @@ -27,6 +27,7 @@ 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.diagnostic.KotlinDiagnosticProvider import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult @@ -40,10 +41,7 @@ import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams import com.itsaky.androidide.models.Range import com.itsaky.androidide.projects.FileManager -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 com.itsaky.androidide.utils.DocumentUtils import com.itsaky.androidide.utils.Environment import kotlinx.coroutines.CoroutineName @@ -58,9 +56,6 @@ import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule -import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule -import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule import org.jetbrains.kotlin.config.JvmTarget import org.jetbrains.kotlin.config.LanguageVersion import org.jetbrains.kotlin.platform.jvm.JvmPlatforms @@ -79,6 +74,7 @@ class KotlinLanguageServer : ILanguageServer { private val scope = CoroutineScope(SupervisorJob() + CoroutineName(KotlinLanguageServer::class.simpleName!!)) + private var projectModel: KotlinProjectModel? = null private var compiler: Compiler? = null private var diagnosticProvider: KotlinDiagnosticProvider? = null private var analyzeJob: Job? = null @@ -96,7 +92,7 @@ class KotlinLanguageServer : ILanguageServer { private val ANALYZE_DEBOUNCE_DELAY = 400.milliseconds const val SERVER_ID = "ide.lsp.kotlin" - private val log = LoggerFactory.getLogger(KotlinLanguageServer::class.java) + private val logger = LoggerFactory.getLogger(KotlinLanguageServer::class.java) } init { @@ -110,6 +106,7 @@ class KotlinLanguageServer : ILanguageServer { override fun shutdown() { EventBus.getDefault().unregister(this) scope.cancel("LSP is being shut down") + diagnosticProvider?.close() compiler?.close() initialized = false } @@ -123,14 +120,7 @@ class KotlinLanguageServer : ILanguageServer { } override fun setupWithProject(workspace: Workspace) { - log.info("setupWithProject called, initialized={}", initialized) - recreateSession(workspace) - initialized = true - } - - private fun recreateSession(workspace: Workspace) { - diagnosticProvider?.close() - compiler?.close() + logger.info("setupWithProject called, initialized={}", initialized) val jdkHome = Environment.JAVA_HOME.toPath() val jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE @@ -144,98 +134,31 @@ class KotlinLanguageServer : ILanguageServer { val jvmPlatform = JvmPlatforms.jvmPlatformByTargetVersion(jvmTarget) - compiler = Compiler( - intellijPluginRoot = intellijPluginRoot, - jdkHome = jdkHome, - jdkRelease = jdkRelease, - languageVersion = LanguageVersion.LATEST_STABLE - ) { - buildKtModuleProvider { - platform = jvmPlatform - - val moduleProjects = - workspace.subProjects - .filterIsInstance() - .filter { it.path != workspace.rootProject.path } - - val bootClassPaths = - moduleProjects - .filterIsInstance() - .flatMap { project -> - project.bootClassPaths - .map { bootClassPath -> - addModule(buildKtLibraryModule { - this.platform = jvmPlatform - this.libraryName = bootClassPath.nameWithoutExtension - addBinaryRoot(bootClassPath.toPath()) - }) - } - } - - val libraryDependencies = - moduleProjects - .flatMap { it.getCompileClasspaths() } - .associateWith { library -> - addModule(buildKtLibraryModule { - this.platform = jvmPlatform - this.libraryName = library.nameWithoutExtension - addBinaryRoot(library.toPath()) - }) - } - - val subprojectsAsModules = mutableMapOf() - - fun getOrCreateModule(project: ModuleProject): KaSourceModule { - subprojectsAsModules[project]?.also { module -> - // a source module already exists for this project - return module - } - - val module = buildKtSourceModule { - this.platform = jvmPlatform - this.moduleName = project.name - addSourceRoots( - project.getSourceDirectories().map { it.toPath() }) - - // always dependent on boot class paths, if any - bootClassPaths.forEach { bootClassPathModule -> - addRegularDependency(bootClassPathModule) - } - - project.getCompileClasspaths(excludeSourceGeneratedClassPath = true) - .forEach { classpath -> - val libDependency = libraryDependencies[classpath] - if (libDependency == null) { - log.error( - "Unable to locate library module for classpath: {}", - libDependency - ) - return@forEach - } - - addRegularDependency(libDependency) - } - - project.getCompileModuleProjects() - .forEach { dependencyModule -> - addRegularDependency(getOrCreateModule(dependencyModule)) - } - } - - subprojectsAsModules[project] = module - return module - } - - moduleProjects.forEach { project -> - addModule(getOrCreateModule(project)) - } - } + if (!initialized) { + logger.info("Creating initial analysis session") + + val model = KotlinProjectModel() + model.update(workspace, jvmPlatform) + this.projectModel = model + + val compiler = Compiler( + projectModel = model, + intellijPluginRoot = intellijPluginRoot, + jdkHome = jdkHome, + jdkRelease = jdkRelease, + languageVersion = LanguageVersion.LATEST_STABLE, + ) + + this.compiler = compiler + this.diagnosticProvider = KotlinDiagnosticProvider(compiler, scope) + } else { + logger.info("Updating project model") + + projectModel?.update(workspace, jvmPlatform) } - diagnosticProvider = KotlinDiagnosticProvider( - compiler = compiler!!, - scope = scope, - ) + initialized = true + logger.info("Kotlin project initialized") } override fun complete(params: CompletionParams?): CompletionResult { @@ -283,10 +206,10 @@ class KotlinLanguageServer : ILanguageServer { } override suspend fun analyze(file: Path): DiagnosticResult { - log.debug("analyze(file={})", file) + logger.debug("analyze(file={})", file) if (!settings.diagnosticsEnabled() || !settings.codeAnalysisEnabled()) { - log.debug( + logger.debug( "analyze() skipped: diagnosticsEnabled={}, codeAnalysisEnabled={}", settings.diagnosticsEnabled(), settings.codeAnalysisEnabled() ) @@ -294,7 +217,7 @@ class KotlinLanguageServer : ILanguageServer { } if (!DocumentUtils.isKotlinFile(file)) { - log.debug("analyze() skipped: not a Kotlin file") + logger.debug("analyze() skipped: not a Kotlin file") return DiagnosticResult.NO_UPDATE } @@ -366,6 +289,6 @@ class KotlinLanguageServer : ILanguageServer { selectedFile = event.selectedFile val uri = event.selectedFile.toUri().toString() - log.debug("onDocumentSelected: uri={}", uri) + logger.debug("onDocumentSelected: uri={}", uri) } } 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 f49753a39f..1b5cc52b0c 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,13 +1,26 @@ package com.itsaky.androidide.lsp.kotlin.compiler +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +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.packages.KotlinPackageProviderFactory import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession -import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISessionBuilder +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.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.com.intellij.core.CoreApplicationEnvironment +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.application.ApplicationManager 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.psi.PsiDocumentManager import org.jetbrains.kotlin.com.intellij.psi.PsiManager import org.jetbrains.kotlin.config.ApiVersion @@ -22,6 +35,7 @@ import org.jetbrains.kotlin.config.messageCollector import org.jetbrains.kotlin.config.moduleName import org.jetbrains.kotlin.config.useFir 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 @@ -36,21 +50,33 @@ import kotlin.io.path.pathString * @param jdkRelease The JDK release version at [jdkHome]. */ class CompilationEnvironment( - intellijPluginRoot: Path, - jdkHome: Path, - jdkRelease: Int, - languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, - enableParserEventSystem: Boolean = true, - configureSession: StandaloneAnalysisAPISessionBuilder.() -> Unit = {} -) : AutoCloseable { - private val disposable = Disposer.newDisposable() - - val session: StandaloneAnalysisAPISession - val parser: KtPsiFactory + val projectModel: KotlinProjectModel, + val intellijPluginRoot: Path, + val jdkHome: Path, + val jdkRelease: Int, + val languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, + val enableParserEventSystem: Boolean = true +) : KotlinProjectModel.ProjectModelListener, AutoCloseable { + private var disposable = Disposer.newDisposable() + + var session: StandaloneAnalysisAPISession + private set + var parser: KtPsiFactory + private set + val psiManager: PsiManager + get() = PsiManager.getInstance(session.project) + val psiDocumentManager: PsiDocumentManager + get() = PsiDocumentManager.getInstance(session.project) - private val envMessageCollector = object: MessageCollector { + val modificationTrackerFactory: KotlinModificationTrackerFactory + get() = session.project.getService(KotlinModificationTrackerFactory::class.java) + + val coreApplicationEnvironment: CoreApplicationEnvironment + get() = session.coreApplicationEnvironment + + private val envMessageCollector = object : MessageCollector { override fun clear() { } @@ -73,39 +99,143 @@ class CompilationEnvironment( } init { - val configuration = CompilerConfiguration().apply { + session = buildSession() + parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) + + projectModel.addListener(this) + } + + private fun buildSession(): StandaloneAnalysisAPISession { + val configuration = createCompilerConfiguration() + + val session = buildStandaloneAnalysisAPISession( + projectDisposable = disposable, + unitTestMode = false, + compilerConfiguration = configuration, + ) { + buildKtModuleProvider { + projectModel.configureModules(this) + } + } + + return session + } + + private fun rebuildSession() { + logger.info("Rebuilding analysis session") + + disposable.dispose() + disposable = Disposer.newDisposable() + + session = buildSession() + parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) + + logger.info("Analysis session rebuilt") + } + + private fun createCompilerConfiguration(): CompilerConfiguration { + return CompilerConfiguration().apply { this.moduleName = JvmProtoBufUtil.DEFAULT_MODULE_NAME this.useFir = true - this.intellijPluginRoot = intellijPluginRoot.pathString + this.intellijPluginRoot = this@CompilationEnvironment.intellijPluginRoot.pathString this.languageVersionSettings = LanguageVersionSettingsImpl( - languageVersion = languageVersion, - apiVersion = ApiVersion.createByLanguageVersion(languageVersion), + languageVersion = this@CompilationEnvironment.languageVersion, + apiVersion = ApiVersion.createByLanguageVersion(this@CompilationEnvironment.languageVersion), analysisFlags = emptyMap(), - specificFeatures = buildMap { - // enable all features - putAll(LanguageFeature.entries.associateWith { LanguageFeature.State.ENABLED }) - } + specificFeatures = LanguageFeature.entries.associateWith { LanguageFeature.State.ENABLED } ) - this.jdkHome = jdkHome.toFile() - this.jdkRelease = jdkRelease + this.jdkHome = this@CompilationEnvironment.jdkHome.toFile() + this.jdkRelease = this@CompilationEnvironment.jdkRelease - this.messageCollector = envMessageCollector + this.messageCollector = this@CompilationEnvironment.envMessageCollector } + } - session = buildStandaloneAnalysisAPISession( - projectDisposable = disposable, - unitTestMode = false, - compilerConfiguration = configuration, - init = configureSession - ) + private fun refreshSourceFiles() { + logger.info("Refreshing source files") - parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) - psiManager = PsiManager.getInstance(session.project) - psiDocumentManager = PsiDocumentManager.getInstance(session.project) + 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 + ) + + registerService( + KotlinPackageProviderFactory::class.java, + KotlinStandalonePackageProviderFactory( + project, + sourceKtFiles + decProviderFactory.getAdditionalCreatedKtFiles() + ) + ) + } + + val modificationTrackerFactory = + project.getService(KotlinModificationTrackerFactory::class.java) as? KotlinStandaloneModificationTrackerFactory? + val sourceModificationTracker = + modificationTrackerFactory?.createProjectWideSourceModificationTracker() as? SimpleModificationTracker? + sourceModificationTracker?.incModificationCount() + } + + logger.info("Refreshed: {} source KtFiles", sourceKtFiles.size) + } + + @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) + } + } + } + + 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) + } + } + } } override fun close() { + projectModel.removeListener(this) disposable.dispose() } + + override fun onProjectModelChanged( + 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 263f554e02..30cf39d8fb 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 @@ -17,13 +17,15 @@ import java.nio.file.Paths import kotlin.io.path.pathString class Compiler( + projectModel: KotlinProjectModel, intellijPluginRoot: Path, jdkHome: Path, jdkRelease: Int, languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, - configureSession: StandaloneAnalysisAPISessionBuilder.() -> Unit = {}, ) : AutoCloseable { private val logger = LoggerFactory.getLogger(Compiler::class.java) + + @Suppress("JoinDeclarationAndAssignment") private val defaultCompilationEnv: CompilationEnvironment val fileSystem: VirtualFileSystem @@ -33,12 +35,12 @@ class Compiler( init { defaultCompilationEnv = CompilationEnvironment( + projectModel = projectModel, intellijPluginRoot = intellijPluginRoot, jdkHome = jdkHome, jdkRelease = jdkRelease, languageVersion = languageVersion, enableParserEventSystem = true, - configureSession = configureSession, ) // must be initialized AFTER the compilation env has been initialized diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt new file mode 100644 index 0000000000..032341591d --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt @@ -0,0 +1,22 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import org.jetbrains.kotlin.com.intellij.openapi.util.ModificationTracker +import java.util.concurrent.atomic.AtomicLong + +class IncrementalModificationTracker : ModificationTracker { + + private val myCounter = AtomicLong(0) + + /** + * Increment the modification count. + */ + fun incModificationCount() = apply { + myCounter.incrementAndGet() + } + + operator fun inc() = incModificationCount() + + override fun getModificationCount(): Long { + return myCounter.get() + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..e78b8646c1 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt @@ -0,0 +1,166 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +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.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.jetbrains.kotlin.config.LanguageVersion +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.slf4j.LoggerFactory + +/** + * Holds the project structure derived from a [Workspace]. + * + * This is the single source of truth for module layout, dependencies, + * and source roots. It knows nothing about analysis sessions — it just + * describes *what* the project looks like. + * + * When the project structure changes (re-sync) or source files change + * (build complete), it notifies registered listeners so they can + * refresh their sessions. + */ +class KotlinProjectModel { + + private val logger = LoggerFactory.getLogger(KotlinProjectModel::class.java) + + private var workspace: Workspace? = null + private var platform: TargetPlatform = JvmPlatforms.defaultJvmPlatform + + private val listeners = mutableListOf() + + /** + * The kind of change that occurred. + */ + enum class ChangeKind { + /** Module structure, dependencies, or platform changed. Full rebuild needed. */ + STRUCTURE, + + /** Only source files within existing roots changed. Incremental refresh possible. */ + SOURCES, + } + + fun interface ProjectModelListener { + fun onProjectModelChanged(model: KotlinProjectModel, changeKind: ChangeKind) + } + + fun addListener(listener: ProjectModelListener) { + listeners.add(listener) + } + + fun removeListener(listener: ProjectModelListener) { + listeners.remove(listener) + } + + /** + * Called when the project is synced (setupWithProject). + * This replaces the entire project structure. + */ + fun update(workspace: Workspace, platform: TargetPlatform) { + this.workspace = workspace + this.platform = platform + notifyListeners(ChangeKind.STRUCTURE) + } + + /** + * Called when a build completes and source files may have changed + * (generated sources added/removed), but the module structure is the same. + */ + fun onSourcesChanged() { + if (workspace == null) { + logger.warn("onSourcesChanged called before project model was initialized") + return + } + 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 + .filterIsInstance() + .filter { it.path != workspace.rootProject.path } + + val bootClassPaths = moduleProjects + .filterIsInstance() + .flatMap { project -> + project.bootClassPaths + .filter { it.exists() } + .map { bootClassPath -> + addModule(buildKtLibraryModule { + this.platform = this@KotlinProjectModel.platform + this.libraryName = bootClassPath.nameWithoutExtension + addBinaryRoot(bootClassPath.toPath()) + }) + } + } + + val libraryDependencies = moduleProjects + .flatMap { it.getCompileClasspaths() } + .filter { it.exists() } + .associateWith { library -> + addModule(buildKtLibraryModule { + this.platform = this@KotlinProjectModel.platform + this.libraryName = library.nameWithoutExtension + addBinaryRoot(library.toPath()) + }) + } + + val subprojectsAsModules = mutableMapOf() + + fun getOrCreateModule(project: ModuleProject): KaSourceModule { + subprojectsAsModules[project]?.let { return it } + + val module = buildKtSourceModule { + this.platform = this@KotlinProjectModel.platform + this.moduleName = project.name + addSourceRoots(project.getSourceDirectories().map { it.toPath() }) + + bootClassPaths.forEach { addRegularDependency(it) } + + project.getCompileClasspaths(excludeSourceGeneratedClassPath = true) + .forEach { classpath -> + val libDep = libraryDependencies[classpath] + 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)) } + } + } + + private fun notifyListeners(changeKind: ChangeKind) { + logger.info("Notifying project listeners for change: {}", changeKind) + listeners.forEach { it.onProjectModelChanged(this, changeKind) } + } +} \ No newline at end of file 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 9b984adaaa..fa8a60ffc2 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 @@ -115,11 +115,16 @@ class KotlinDiagnosticProvider( } } + internal fun clearTimestamps() { + analyzeTimestamps.clear() + } + internal fun clearTimestamp(file: Path) { analyzeTimestamps.remove(file) } override fun close() { + clearTimestamps() scope.cancelIfActive("diagnostic provider is being destroyed") } } From dd3d519e6b4258f8a89200f1dda97d8387fae960 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 31 Mar 2026 21:03:29 +0530 Subject: [PATCH 19/58] fix: dispatch build-related events from GradleBuildService Signed-off-by: Akash Yadav --- .../editor/ProjectHandlerActivity.kt | 3 +- .../analytics/gradle/BuildMetric.kt | 1 + .../services/builder/GradleBuildService.kt | 52 ++++++++++++------- eventbus-events/build.gradle.kts | 1 + .../androidide/eventbus/events/BuildEvent.kt | 32 ++++++++++++ .../tooling/api/messages/BuildId.kt | 21 +++++++- .../testing/tooling/ToolingApiTestLauncher.kt | 2 + 7 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/BuildEvent.kt diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt index 017ca90126..78d6cbc64d 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt @@ -66,6 +66,7 @@ import com.itsaky.androidide.services.builder.GradleBuildServiceConnnection import com.itsaky.androidide.services.builder.gradleDistributionParams import com.itsaky.androidide.tooling.api.messages.AndroidInitializationParams import com.itsaky.androidide.tooling.api.messages.BuildId +import com.itsaky.androidide.tooling.api.messages.BuildRunType import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.tooling.api.messages.result.InitializeResult import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult @@ -565,7 +566,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() { projectDir = projectDir, buildVariants = buildVariants, needsGradleSync = needsSync, - buildId = buildService.nextBuildId(), + buildId = buildService.nextBuildId(BuildRunType.ProjectSync), ), ) diff --git a/app/src/main/java/com/itsaky/androidide/analytics/gradle/BuildMetric.kt b/app/src/main/java/com/itsaky/androidide/analytics/gradle/BuildMetric.kt index bc4be3ee79..06a6b43372 100644 --- a/app/src/main/java/com/itsaky/androidide/analytics/gradle/BuildMetric.kt +++ b/app/src/main/java/com/itsaky/androidide/analytics/gradle/BuildMetric.kt @@ -19,5 +19,6 @@ abstract class BuildMetric : Metric { Bundle().apply { putString("build_session_id", buildId.buildSessionId) putLong("build_id", buildId.buildId) + putString("run_type", buildId.runType.typeName) } } diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt index 2a2732e68b..85f95e5324 100644 --- a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt +++ b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt @@ -33,6 +33,8 @@ import com.itsaky.androidide.analytics.gradle.BuildCompletedMetric import com.itsaky.androidide.analytics.gradle.BuildStartedMetric import com.itsaky.androidide.app.BaseApplication import com.itsaky.androidide.app.IDEApplication +import com.itsaky.androidide.eventbus.events.BuildCompletedEvent +import com.itsaky.androidide.eventbus.events.BuildStartedEvent import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lsp.java.debug.JdwpOptions import com.itsaky.androidide.managers.ToolsManager @@ -52,6 +54,7 @@ import com.itsaky.androidide.tooling.api.GradlePluginConfig.PROPERTY_LOGSENDER_E import com.itsaky.androidide.tooling.api.IToolingApiClient import com.itsaky.androidide.tooling.api.IToolingApiServer import com.itsaky.androidide.tooling.api.messages.BuildId +import com.itsaky.androidide.tooling.api.messages.BuildRunType import com.itsaky.androidide.tooling.api.messages.ClientGradleBuildConfig import com.itsaky.androidide.tooling.api.messages.GradleBuildParams import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams @@ -77,6 +80,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.future.await import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus import org.koin.android.ext.android.inject import org.slf4j.LoggerFactory import java.io.File @@ -162,10 +166,11 @@ class GradleBuildService : } } ?: "unknown" - internal fun nextBuildId(): BuildId = + internal fun nextBuildId(runType: BuildRunType): BuildId = BuildId( buildSessionId = buildSessionId, buildId = buildId.incrementAndGet(), + runType = runType, ) companion object { @@ -360,7 +365,8 @@ class GradleBuildService : var newTuningConfig: GradleTuningConfig? = null @Suppress("SimplifyBooleanWithConstants") - val extraArgs = getGradleExtraArgs(enableJdwp = JdwpOptions.JDWP_ENABLED && isDebugBuild) + val extraArgs = + getGradleExtraArgs(enableJdwp = JdwpOptions.JDWP_ENABLED && isDebugBuild) var buildParams = if (FeatureFlags.isExperimentsEnabled) { @@ -402,6 +408,11 @@ class GradleBuildService : ), ) + EventBus.getDefault() + .post( + BuildStartedEvent(buildInfo) + ) + eventListener?.prepareBuild(buildInfo) return@supplyAsync ClientGradleBuildConfig( @@ -412,33 +423,38 @@ class GradleBuildService : override fun onBuildSuccessful(result: BuildResult) { updateNotification(getString(R.string.build_status_sucess), false) - val buildType = getBuildType(result.tasks) - analyticsManager.trackBuildCompleted( - metric = - BuildCompletedMetric( - buildId = result.buildId, - isSuccess = true, - buildType = buildType, - buildResult = result, - ), - ) + dispatchBuildResult(result, true) eventListener?.onBuildSuccessful(result.tasks) } override fun onBuildFailed(result: BuildResult) { updateNotification(getString(R.string.build_status_failed), false) + dispatchBuildResult(result, false) + eventListener?.onBuildFailed(result.tasks) + } + + private fun dispatchBuildResult( + result: BuildResult, + isSuccess: Boolean, + ) { val buildType = getBuildType(result.tasks) analyticsManager.trackBuildCompleted( metric = BuildCompletedMetric( buildId = result.buildId, - isSuccess = false, + isSuccess = isSuccess, buildType = buildType, buildResult = result, ), ) - eventListener?.onBuildFailed(result.tasks) + + EventBus.getDefault() + .post( + BuildCompletedEvent( + result = result, + ) + ) } override fun onProgressEvent(event: ProgressEvent) { @@ -574,7 +590,7 @@ class GradleBuildService : message = TaskExecutionMessage( tasks = tasks, - buildId = nextBuildId(), + buildId = nextBuildId(BuildRunType.TaskRun), ), ) @@ -610,9 +626,9 @@ class GradleBuildService : } catch (e: Throwable) { if (BuildPreferences.isScanEnabled && ( - e.toString().contains(ERROR_GRADLE_ENTERPRISE_PLUGIN) || - e.toString().contains(ERROR_COULD_NOT_FIND_GRADLE) - ) + e.toString().contains(ERROR_GRADLE_ENTERPRISE_PLUGIN) || + e.toString().contains(ERROR_COULD_NOT_FIND_GRADLE) + ) ) { BuildPreferences.isScanEnabled = false diff --git a/eventbus-events/build.gradle.kts b/eventbus-events/build.gradle.kts index 8a48563076..ab62bdeea3 100644 --- a/eventbus-events/build.gradle.kts +++ b/eventbus-events/build.gradle.kts @@ -29,6 +29,7 @@ android { dependencies { implementation(libs.common.kotlin) implementation(projects.shared) + implementation(projects.subprojects.toolingApi) implementation(projects.logger) api(projects.eventbus) diff --git a/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/BuildEvent.kt b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/BuildEvent.kt new file mode 100644 index 0000000000..916a0954e6 --- /dev/null +++ b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/BuildEvent.kt @@ -0,0 +1,32 @@ +package com.itsaky.androidide.eventbus.events + +import com.itsaky.androidide.tooling.api.messages.BuildId +import com.itsaky.androidide.tooling.api.messages.result.BuildInfo +import com.itsaky.androidide.tooling.api.messages.result.BuildResult + +/** + * Events dispatched from the IDE's build service. + * + * @property buildId The build identifier. + */ +abstract class BuildEvent( + val buildId: BuildId, +) : Event() + +/** + * Event dispatched when a Gradle build is started in the IDE. + * + * @property buildInfo Info about the build. + */ +class BuildStartedEvent( + val buildInfo: BuildInfo, +): BuildEvent(buildInfo.buildId) + +/** + * Event dispatched when a Gradle build is completed in the IDE. + * + * @property result The result of the Gradle build. + */ +class BuildCompletedEvent( + val result: BuildResult, +): BuildEvent(result.buildId) diff --git a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/BuildId.kt b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/BuildId.kt index fb5dadfda9..2984af70ea 100644 --- a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/BuildId.kt +++ b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/BuildId.kt @@ -10,8 +10,27 @@ import java.io.Serializable data class BuildId( val buildSessionId: String, val buildId: Long, + val runType: BuildRunType, ) : Serializable { companion object { - val Unknown = BuildId("unknown", -1) + val Unknown = BuildId("unknown", -1, BuildRunType.TaskRun) } } + +/** + * The type of Gradle build run. + */ +enum class BuildRunType( + val typeName: String, +) { + + /** + * Gradle build for project synchronization. + */ + ProjectSync("sync"), + + /** + * Gradle build for running one or more tasks. + */ + TaskRun("taskRun"), +} diff --git a/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/ToolingApiTestLauncher.kt b/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/ToolingApiTestLauncher.kt index f32bcee758..e9e72ff7bf 100644 --- a/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/ToolingApiTestLauncher.kt +++ b/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/ToolingApiTestLauncher.kt @@ -23,6 +23,7 @@ import com.itsaky.androidide.testing.tooling.models.ToolingApiTestScope import com.itsaky.androidide.tooling.api.IToolingApiClient import com.itsaky.androidide.tooling.api.IToolingApiServer import com.itsaky.androidide.tooling.api.messages.BuildId +import com.itsaky.androidide.tooling.api.messages.BuildRunType import com.itsaky.androidide.tooling.api.messages.ClientGradleBuildConfig import com.itsaky.androidide.tooling.api.messages.GradleBuildParams import com.itsaky.androidide.tooling.api.messages.GradleDistributionParams @@ -93,6 +94,7 @@ object ToolingApiTestLauncher { BuildId( buildSessionId = UUID.randomUUID().toString(), buildId = Random.nextLong(), + runType = BuildRunType.ProjectSync, ), ), log: Logger = LoggerFactory.getLogger("BuildOutputLogger"), From d6defa68862cd65e73839331d53ee9bb1ff887d5 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 31 Mar 2026 20:39:50 +0530 Subject: [PATCH 20/58] feat: introduct KtFileManager Handles document events to manage instances of in-memory KtFile that can be used by various Kt LSP components (like diagnostics provider, code completions) to re-use already parsed KtFile instances Signed-off-by: Akash Yadav --- .../lsp/kotlin/FileEventConsumer.kt | 12 ++ .../lsp/kotlin/KotlinLanguageServer.kt | 38 +++- .../androidide/lsp/kotlin/KtFileManager.kt | 193 ++++++++++++++++++ .../kotlin/compiler/CompilationEnvironment.kt | 8 + .../lsp/kotlin/compiler/Compiler.kt | 28 ++- .../diagnostic/KotlinDiagnosticProvider.kt | 146 ++++--------- 6 files changed, 303 insertions(+), 122 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/FileEventConsumer.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KtFileManager.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/FileEventConsumer.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/FileEventConsumer.kt new file mode 100644 index 0000000000..0fc2feab55 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/FileEventConsumer.kt @@ -0,0 +1,12 @@ +package com.itsaky.androidide.lsp.kotlin + +import java.nio.file.Path + +interface FileEventConsumer { + + fun onFileOpened(path: Path, content: String) + fun onFileClosed(path: Path) + + fun onFileContentChanged(path: Path, content: String) + fun onFileSaved(path: Path) +} \ No newline at end of file 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 da9a96664a..5085d10fb3 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.diagnostic.KotlinDiagnosticProvider +import com.itsaky.androidide.lsp.kotlin.diagnostic.collectDiagnosticsFor import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.DefinitionParams @@ -76,7 +77,6 @@ class KotlinLanguageServer : ILanguageServer { CoroutineScope(SupervisorJob() + CoroutineName(KotlinLanguageServer::class.simpleName!!)) private var projectModel: KotlinProjectModel? = null private var compiler: Compiler? = null - private var diagnosticProvider: KotlinDiagnosticProvider? = null private var analyzeJob: Job? = null override val serverId: String = SERVER_ID @@ -106,7 +106,6 @@ class KotlinLanguageServer : ILanguageServer { override fun shutdown() { EventBus.getDefault().unregister(this) scope.cancel("LSP is being shut down") - diagnosticProvider?.close() compiler?.close() initialized = false } @@ -150,7 +149,6 @@ class KotlinLanguageServer : ILanguageServer { ) this.compiler = compiler - this.diagnosticProvider = KotlinDiagnosticProvider(compiler, scope) } else { logger.info("Updating project model") @@ -221,7 +219,7 @@ class KotlinLanguageServer : ILanguageServer { return DiagnosticResult.NO_UPDATE } - return diagnosticProvider?.analyze(file) + return compiler?.compilationEnvironmentFor(file)?.collectDiagnosticsFor(file) ?: DiagnosticResult.NO_UPDATE } @@ -232,6 +230,11 @@ class KotlinLanguageServer : ILanguageServer { return } + compiler?.compilationEnvironmentFor(event.openedFile)?.apply { + val content = FileManager.getDocumentContents(event.openedFile) + fileManager.onFileOpened(event.openedFile, content) + } + selectedFile = event.openedFile debouncingAnalyze() } @@ -262,6 +265,13 @@ class KotlinLanguageServer : ILanguageServer { if (!DocumentUtils.isKotlinFile(event.changedFile)) { return } + + 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) + } + debouncingAnalyze() } @@ -272,13 +282,29 @@ class KotlinLanguageServer : ILanguageServer { return } - diagnosticProvider?.clearTimestamp(event.closedFile) + compiler?.compilationEnvironmentFor(event.closedFile)?.apply { + fileManager.onFileClosed(event.closedFile) + fileManager.clearAnalyzeTimestampOf(event.closedFile) + } + if (FileManager.getActiveDocumentCount() == 0) { selectedFile = null analyzeJob?.cancel("No active files") } } + @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 new file mode 100644 index 0000000000..2941cfb2c1 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KtFileManager.kt @@ -0,0 +1,193 @@ +package com.itsaky.androidide.lsp.kotlin + +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) + return + } + + 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? = entries[path] + + 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 1b5cc52b0c..f9b20ebc3a 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,5 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler +import com.itsaky.androidide.lsp.kotlin.FileEventConsumer +import com.itsaky.androidide.lsp.kotlin.KtFileManager import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory @@ -61,9 +63,13 @@ class CompilationEnvironment( var session: StandaloneAnalysisAPISession private set + var parser: KtPsiFactory private set + var fileManager: KtFileManager + private set + val psiManager: PsiManager get() = PsiManager.getInstance(session.project) @@ -101,6 +107,7 @@ class CompilationEnvironment( init { session = buildSession() parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) + fileManager = KtFileManager(parser, psiManager, psiDocumentManager) projectModel.addListener(this) } @@ -225,6 +232,7 @@ class CompilationEnvironment( } override fun close() { + fileManager.close() projectModel.removeListener(this) disposable.dispose() } 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 30cf39d8fb..a02e6ebe44 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,6 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler -import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISessionBuilder +import com.itsaky.androidide.lsp.kotlin.FileEventConsumer +import com.itsaky.androidide.utils.DocumentUtils import org.jetbrains.kotlin.com.intellij.lang.Language import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager @@ -14,6 +15,7 @@ import org.jetbrains.kotlin.psi.KtPsiFactory import org.slf4j.LoggerFactory import java.nio.file.Path import java.nio.file.Paths +import kotlin.io.path.extension import kotlin.io.path.pathString class Compiler( @@ -44,8 +46,19 @@ class Compiler( ) // must be initialized AFTER the compilation env has been initialized - fileSystem = VirtualFileManager.getInstance() - .getFileSystem(StandardFileSystems.FILE_PROTOCOL) + fileSystem = + VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) + } + + fun compilationKindFor(file: Path): CompilationKind { + // TODO: This should return a different environment for Kotlin script files + return CompilationKind.Default + } + + fun compilationEnvironmentFor(file: Path): CompilationEnvironment? { + if (!DocumentUtils.isKotlinFile(file)) return null + + return compilationEnvironmentFor(compilationKindFor(file)) } fun compilationEnvironmentFor(compilationKind: CompilationKind): CompilationEnvironment = @@ -66,11 +79,7 @@ class Compiler( require(!content.contains('\r')) val psiFile = psiFileFactoryFor(compilationKind).createFileFromText( - file.pathString, - language, - content, - true, - false + file.pathString, language, content, true, false ) check(psiFile.virtualFile != null) { "No virtual-file associated with newly created psiFile" @@ -83,8 +92,7 @@ class Compiler( content: String, file: Path = Paths.get("dummy.virtual.kt"), compilationKind: CompilationKind = CompilationKind.Default - ): KtFile = - createPsiFileFor(content, file, KotlinLanguage.INSTANCE, compilationKind) as KtFile + ): KtFile = createPsiFileFor(content, file, KotlinLanguage.INSTANCE, compilationKind) as KtFile override fun close() { defaultCompilationEnv.close() 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 fa8a60ffc2..ac2c38672f 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,131 +1,66 @@ package com.itsaky.androidide.lsp.kotlin.diagnostic -import com.itsaky.androidide.lsp.kotlin.compiler.CompilationKind -import com.itsaky.androidide.lsp.kotlin.compiler.Compiler +import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment 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 com.itsaky.androidide.tasks.cancelIfActive import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope import org.jetbrains.kotlin.analysis.api.KaExperimentalApi -import org.jetbrains.kotlin.analysis.api.analyzeCopy +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.analysis.api.projectStructure.KaDanglingFileModule -import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode -import org.jetbrains.kotlin.com.intellij.openapi.application.ApplicationManager 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.jetbrains.kotlin.com.intellij.testFramework.LightVirtualFile -import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory import java.nio.file.Path -import java.time.Instant -import java.util.concurrent.ConcurrentHashMap -import kotlin.io.path.name -import kotlin.io.path.pathString -import org.jetbrains.kotlin.analysis.api.analyze as ktAnalyze - -class KotlinDiagnosticProvider( - private val compiler: Compiler, - private val scope: CoroutineScope -) : AutoCloseable { - - companion object { - private val logger = LoggerFactory.getLogger(KotlinDiagnosticProvider::class.java) +import kotlin.time.Clock +import kotlin.time.toKotlinInstant + +private val logger = LoggerFactory.getLogger("KotlinDiagnosticProvider") + +fun CompilationEnvironment.collectDiagnosticsFor(file: Path): DiagnosticResult = try { + logger.info("Analyzing file: {}", file) + return doAnalyze(file) +} catch (err: Throwable) { + if (err is CancellationException) { + logger.debug("analysis cancelled") + throw err } + logger.error("An error occurred analyzing file: {}", file, err) + return DiagnosticResult.NO_UPDATE +} - private val analyzeTimestamps = ConcurrentHashMap() - - fun analyze(file: Path): DiagnosticResult = - try { - logger.info("Analyzing file: {}", file) - return doAnalyze(file) - } catch (err: Throwable) { - if (err is CancellationException) { - logger.debug("analysis cancelled") - throw err - } - logger.error("An error occurred analyzing file: {}", file, err) - return DiagnosticResult.NO_UPDATE - } - - @OptIn(KaExperimentalApi::class) - private fun doAnalyze(file: Path): DiagnosticResult { - val modifiedAt = FileManager.getLastModified(file) - val analyzedAt = analyzeTimestamps[file] - if (analyzedAt?.isAfter(modifiedAt) == true) { - logger.debug("Skipping analysis. File unmodified.") - return DiagnosticResult.NO_UPDATE - } - - logger.info("fetch document contents") - val fileContents = FileManager.getDocumentContents(file) - .replace("\r", "") - - val env = compiler.compilationEnvironmentFor(CompilationKind.Default) - val virtualFile = compiler.fileSystem.refreshAndFindFileByPath(file.pathString) - if (virtualFile == null) { - logger.warn("Unable to find virtual file for path: {}", file.pathString) - return DiagnosticResult.NO_UPDATE - } - - val ktFile = env.psiManager.findFile(virtualFile) - if (ktFile == null) { - logger.warn("Unable to find KtFile for path: {}", file.pathString) - return DiagnosticResult.NO_UPDATE - } - - if (ktFile !is KtFile) { - logger.warn( - "Expected KtFile, but found {} for path:{}", - ktFile.javaClass, - file.pathString - ) - return DiagnosticResult.NO_UPDATE - } - - val inMemoryPsi = compiler.defaultKotlinParser - .createFile(file.name, fileContents) - inMemoryPsi.originalFile = ktFile - - val rawDiagnostics = analyzeCopy( - useSiteElement = inMemoryPsi, - resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, - ) { - logger.info("ktFile.text={}", inMemoryPsi.text) - inMemoryPsi.collectDiagnostics(filter = KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) - } - - logger.info("Found {} diagnostics", rawDiagnostics.size) - - return DiagnosticResult( - file = file, - diagnostics = rawDiagnostics.map { rawDiagnostic -> - rawDiagnostic.toDiagnosticItem() - } - ).also { - analyzeTimestamps[file] = Instant.now() - } +@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 } - internal fun clearTimestamps() { - analyzeTimestamps.clear() + val analyzedAt = managed.analyzeTimestamp + val modifiedAt = FileManager.getLastModified(file) + if (analyzedAt > modifiedAt.toKotlinInstant()) { + logger.debug("Skipping analysis. File unmodified.") + return DiagnosticResult.NO_UPDATE } - internal fun clearTimestamp(file: Path) { - analyzeTimestamps.remove(file) + val rawDiagnostics = managed.analyze { ktFile -> + ktFile.collectDiagnostics(filter = KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) } - override fun close() { - clearTimestamps() - scope.cancelIfActive("diagnostic provider is being destroyed") + logger.info("Found {} diagnostics", rawDiagnostics.size) + + return DiagnosticResult( + file = file, diagnostics = rawDiagnostics.map { rawDiagnostic -> + rawDiagnostic.toDiagnosticItem() + }).also { + managed.analyzeTimestamp = Clock.System.now() } } @@ -150,8 +85,8 @@ private fun KaSeverity.toDiagnosticSeverity(): DiagnosticSeverity { } private fun TextRange.toRange(containingFile: PsiFile): Range { - val doc = PsiDocumentManager.getInstance(containingFile.project) - .getDocument(containingFile) ?: return Range.NONE + val doc = PsiDocumentManager.getInstance(containingFile.project).getDocument(containingFile) + ?: return Range.NONE val startLine = doc.getLineNumber(startOffset) val startCol = startOffset - doc.getLineStartOffset(startLine) val endLine = doc.getLineNumber(endOffset) @@ -161,8 +96,7 @@ private fun TextRange.toRange(containingFile: PsiFile): Range { line = startLine, column = startCol, index = startOffset, - ), - end = Position( + ), end = Position( line = endLine, column = endCol, index = endOffset, From 466689312bd50bb40073a3a81d960fd9b8735eee Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 1 Apr 2026 18:59:01 +0530 Subject: [PATCH 21/58] fix: add initial K2-backed scope code completions Signed-off-by: Akash Yadav --- .../lsp/java/edits/BaseJavaEditHandler.kt | 14 +- lsp/kotlin/build.gradle.kts | 1 + .../lsp/kotlin/KotlinLanguageServer.kt | 11 +- .../completion/BaseKotlinEditHandler.kt | 23 ++ .../kotlin/completion/CompletionContext.kt | 17 + .../kotlin/completion/KotlinCompletionItem.kt | 50 +++ .../kotlin/completion/KotlinCompletions.kt | 338 ++++++++++++++++++ 7 files changed, 445 insertions(+), 9 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt index 71db6f6b05..4f12c45336 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt @@ -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) + } } diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index 8af6820538..66f2a74f4b 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -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) 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 5085d10fb3..3bd433a429 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 @@ -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 @@ -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 { @@ -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) } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt new file mode 100644 index 0000000000..81479a265e --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt @@ -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) + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt new file mode 100644 index 0000000000..a87b6891a9 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt @@ -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, +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt new file mode 100644 index 0000000000..9847201e1b --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt @@ -0,0 +1,50 @@ +package com.itsaky.androidide.lsp.kotlin.completion + +import com.itsaky.androidide.lsp.edits.IEditHandler +import com.itsaky.androidide.lsp.models.Command +import com.itsaky.androidide.lsp.models.CompletionItem +import com.itsaky.androidide.lsp.models.CompletionItemKind +import com.itsaky.androidide.lsp.models.ICompletionData +import com.itsaky.androidide.lsp.models.InsertTextFormat +import com.itsaky.androidide.lsp.models.MatchLevel +import com.itsaky.androidide.lsp.models.TextEdit + +class KotlinCompletionItem( + ideLabel: String, + detail: String, + insertText: String?, + insertTextFormat: InsertTextFormat?, + sortText: String?, + command: Command?, + completionKind: CompletionItemKind, + matchLevel: MatchLevel, + additionalTextEdits: List?, + data: ICompletionData?, + editHandler: IEditHandler = BaseKotlinEditHandler() +) : CompletionItem( + ideLabel, + detail, + insertText, + insertTextFormat, + sortText, + command, + completionKind, + matchLevel, + additionalTextEdits, + data, + editHandler +) { + + constructor() : this( + "", // label + "", // detail + null, // insertText + null, // insertTextFormat + null, // sortText + null, // command + CompletionItemKind.NONE, // kind + MatchLevel.NO_MATCH, // match level + ArrayList(), // additionalEdits + null // data + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..5505c9195a --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt @@ -0,0 +1,338 @@ +package com.itsaky.androidide.lsp.kotlin.completion + +import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment +import com.itsaky.androidide.lsp.models.Command +import com.itsaky.androidide.lsp.models.CompletionItem +import com.itsaky.androidide.lsp.models.CompletionItemKind +import com.itsaky.androidide.lsp.models.CompletionParams +import com.itsaky.androidide.lsp.models.CompletionResult +import com.itsaky.androidide.lsp.models.InsertTextFormat +import com.itsaky.androidide.projects.FileManager +import kotlinx.coroutines.CancellationException +import org.jetbrains.kotlin.analysis.api.KaContextParameterApi +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.analyzeCopy +import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode +import org.jetbrains.kotlin.analysis.api.renderer.types.KaTypeRenderer +import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource +import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind +import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaClassifierSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaEnumEntrySymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaLocalVariableSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaPropertySymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaTypeParameterSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol +import org.jetbrains.kotlin.analysis.api.symbols.name +import org.jetbrains.kotlin.analysis.api.types.KaType +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtQualifiedExpression +import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression +import org.jetbrains.kotlin.psi.psiUtil.getParentOfType +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.types.Variance +import org.slf4j.LoggerFactory + +private const val KT_COMPLETION_PLACEHOLDER = "KT_COMPLETION_PLACEHOLDER" + +private val logger = LoggerFactory.getLogger("KotlinCompletions") + +/** + * Provide code completion for the given completion parameters. + * + * @param CompilationEnvironment The compilation environment to use for the code completion. + * @param params The completion parameters. + * @return The completion result. + */ +fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult { + val managedFile = fileManager.getOpenFile(params.file) + if (managedFile == null) { + logger.warn("No managed file for {}", params.file) + return CompletionResult.EMPTY + } + + // Need to use the original document contents here, instead of + // managedFile.inMemoryKtFile.text + val originalText = FileManager.getDocumentContents(params.file) + val requestPosition = params.position + val completionOffset = requestPosition.requireIndex() + val prefix = params.requirePrefix() + val partial = partialIdentifier(prefix) + + // insert placeholder to fix broken trees + val textWithPlaceholder = buildString { + append(originalText, 0, completionOffset) + append(KT_COMPLETION_PLACEHOLDER) + append(originalText, completionOffset, originalText.length) + } + + val completionKtFile = managedFile.createInMemoryFileWithContent(parser, textWithPlaceholder) + val elementAtOffset = completionKtFile.findElementAt(completionOffset) + + if (elementAtOffset == null) { + logger.error("Unable to locate element at position {}", requestPosition) + return CompletionResult.EMPTY + } + + return try { + analyzeCopy( + useSiteElement = completionKtFile, + resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, + ) { + val completionContext = determineCompletionContext(elementAtOffset) + val items = mutableListOf() + + when (completionContext) { + CompletionContext.Scope -> collectScopeCompletions( + element = elementAtOffset, + file = completionKtFile, + partial = partial, + to = items + ) + + CompletionContext.Member -> collectMemberCompletions( + element = elementAtOffset, + partial = partial, + to = items + ) + } + + CompletionResult(items) + } + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + + logger.warn("An error occurred while computing completions for {}", params.file) + return CompletionResult.EMPTY + } +} + +private fun KaSession.collectMemberCompletions( + element: PsiElement, + partial: String, + to: MutableList +) { + val qualifiedExpr = element.getParentOfType(strict = false) + if (qualifiedExpr == null) { + logger.error("No qualified expression found requested position") + return + } + + val receiver = qualifiedExpr.receiverExpression + val receiverType = receiver.expressionType + + if (receiverType == null) { + logger.error("Unable to find receiver expression type") + return + } + + collectMembersFromType(receiverType, partial, to) + + if (qualifiedExpr is KtSafeQualifiedExpression) { + val nonNullType = receiverType.withNullability(isMarkedNullable = false) + collectMembersFromType(nonNullType, partial, to) + } +} + +private fun KaSession.collectMembersFromType( + receiverType: KaType, + partial: String, + to: MutableList +) { + +} + +private fun KaSession.collectScopeCompletions( + element: PsiElement, + file: KtFile, + partial: String, + to: MutableList +) { + // Find the nearest KtElement parent for scope resolution + val ktElement = element.getParentOfType(strict = false) + if (ktElement == null) { + logger.error("Cannot find parent of element {} with partial {}", element, partial) + return + } + + logger.info( + "Complete scope members of {}: [{}] matching '{}'", + ktElement, + ktElement.text, + partial + ) + + val scopeContext = file.scopeContext(ktElement) + val compositeScope = scopeContext.compositeScope() + + compositeScope.callables { name -> matchesPrefix(name, partial) } + .forEach { symbol -> + val item = callableSymbolToCompletionItem(symbol, partial) + if (item != null) { + to += item + } + } + + compositeScope.classifiers { name -> matchesPrefix(name, partial) } + .forEach { symbol -> + val item = classifierSymbolToCompletionItem(symbol, partial) + if (item != null) { + to += item + } + } +} + +private fun determineCompletionContext(element: PsiElement): CompletionContext { + // Walk up to find a qualified expression where we're the selector + val dotExpr = element.getParentOfType(strict = false) + if (dotExpr != null && isInSelectorPosition(element, dotExpr)) { + return CompletionContext.Member + } + + val safeExpr = element.getParentOfType(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 +} + +@OptIn(KaExperimentalApi::class) +private fun KaSession.callableSymbolToCompletionItem( + symbol: KaCallableSymbol, + partial: String +): CompletionItem? { + val item = createSymbolCompletionItem(symbol, partial) ?: return null + val name = item.ideLabel + item.overrideTypeText = renderName(symbol.returnType) + + when (symbol) { + is KaNamedFunctionSymbol -> { + val params = symbol.valueParameters.joinToString(", ") { param -> + "${param.name.asString()}: ${renderName(param.returnType)}" + } + + val hasParams = symbol.valueParameters.isNotEmpty() + + item.detail = "${name}($params)" + item.insertTextFormat = InsertTextFormat.SNIPPET + item.insertText = if (hasParams) { + "${name}($0)" + } else { + "${name}()$0" + } + + if (hasParams) { + item.command = Command("Trigger parameter hints", Command.TRIGGER_PARAMETER_HINTS) + } + + // TODO(itsaky): provide method completion data in order to show API info + // in completion items + } + + // TODO: For properties, we can check if they're a compile-time constant + // and include that constant value in the "detail" field of the + // completion item + + else -> {} + } + + return item +} + +@OptIn(KaExperimentalApi::class) +private fun KaSession.classifierSymbolToCompletionItem( + symbol: KaClassifierSymbol, + partial: String +): CompletionItem? { + val item = createSymbolCompletionItem(symbol, partial) ?: return null + item.detail = when (symbol) { + is KaClassSymbol -> symbol.classId?.asFqNameString() ?: "" + is KaTypeAliasSymbol -> renderName(symbol.expandedType, KaTypeRendererForSource.WITH_QUALIFIED_NAMES) + is KaTypeParameterSymbol -> item.ideLabel + } + return item +} + +private fun KaSession.createSymbolCompletionItem( + symbol: KaSymbol, + partial: String +): CompletionItem? { + val name = symbol.name?.asString() ?: return null + + val item = KotlinCompletionItem() + item.ideLabel = name + item.completionKind = kindOf(symbol) + item.matchLevel = CompletionItem.matchLevel(name, partial) + + return item +} + +private fun KaSession.kindOf(symbol: KaSymbol): CompletionItemKind { + return when (symbol) { + is KaClassSymbol -> when (symbol.classKind) { + KaClassKind.CLASS -> CompletionItemKind.CLASS + KaClassKind.ENUM_CLASS -> CompletionItemKind.ENUM + KaClassKind.ANNOTATION_CLASS -> CompletionItemKind.ANNOTATION_TYPE + KaClassKind.OBJECT -> CompletionItemKind.CLASS + KaClassKind.COMPANION_OBJECT -> CompletionItemKind.CLASS + KaClassKind.INTERFACE -> CompletionItemKind.INTERFACE + KaClassKind.ANONYMOUS_OBJECT -> CompletionItemKind.CLASS + } + + is KaTypeParameterSymbol -> CompletionItemKind.TYPE_PARAMETER + is KaTypeAliasSymbol -> CompletionItemKind.CLASS + is KaFunctionSymbol -> when (symbol) { + is KaConstructorSymbol -> CompletionItemKind.CONSTRUCTOR + else -> CompletionItemKind.METHOD + } + + is KaPropertySymbol -> CompletionItemKind.PROPERTY + is KaLocalVariableSymbol -> CompletionItemKind.VARIABLE + is KaValueParameterSymbol -> CompletionItemKind.VARIABLE + is KaEnumEntrySymbol -> CompletionItemKind.ENUM_MEMBER + else -> CompletionItemKind.NONE + } +} + +@OptIn(KaExperimentalApi::class, KaContextParameterApi::class) +private fun KaSession.renderName( + type: KaType, + renderer: KaTypeRenderer = KaTypeRendererForSource.WITH_SHORT_NAMES, + position: Variance = Variance.INVARIANT +): String { + return type.run { + render(renderer, position) + } +} + +private fun partialIdentifier(prefix: String): String { + return prefix.takeLastWhile { char -> Character.isJavaIdentifierPart(char) } +} + +private fun matchesPrefix(name: Name, partial: String): Boolean { + if (partial.isEmpty()) return true + return name.asString().startsWith(partial, ignoreCase = true) +} From 2f982a73bfbed7e31c4ffd3baf26b5c62eed0b8a Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 2 Apr 2026 17:32:28 +0530 Subject: [PATCH 22/58] feat: add member completions backed by K2 Signed-off-by: Akash Yadav --- .../kotlin/completion/KotlinCompletions.kt | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) 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 5505c9195a..07824fceb7 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 @@ -31,6 +31,7 @@ import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaTypeParameterSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol import org.jetbrains.kotlin.analysis.api.symbols.name +import org.jetbrains.kotlin.analysis.api.types.KaClassType import org.jetbrains.kotlin.analysis.api.types.KaType import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.name.Name @@ -147,12 +148,26 @@ private fun KaSession.collectMemberCompletions( } } +@OptIn(KaExperimentalApi::class) private fun KaSession.collectMembersFromType( receiverType: KaType, partial: String, to: MutableList ) { + val typeScope = receiverType.scope + if (typeScope != null) { + to += toCompletionItems(typeScope.getCallableSignatures { name -> matchesPrefix(name, partial) }.map { it.symbol }, partial) + to += toCompletionItems(typeScope.getClassifierSymbols { name -> matchesPrefix(name, partial) }, partial) + return + } + + // fallback approach when typeScope is not available + val classType = receiverType as? KaClassType ?: return + val classSymbol = classType.symbol as? KaClassSymbol ?: return + val memberScope = classSymbol.memberScope + to += toCompletionItems(memberScope.callables { name -> matchesPrefix(name, partial) }, partial) + to += toCompletionItems(memberScope.classifiers { name -> matchesPrefix(name, partial) }, partial) } private fun KaSession.collectScopeCompletions( @@ -178,23 +193,22 @@ private fun KaSession.collectScopeCompletions( val scopeContext = file.scopeContext(ktElement) val compositeScope = scopeContext.compositeScope() - compositeScope.callables { name -> matchesPrefix(name, partial) } - .forEach { symbol -> - val item = callableSymbolToCompletionItem(symbol, partial) - if (item != null) { - to += item - } - } - - compositeScope.classifiers { name -> matchesPrefix(name, partial) } - .forEach { symbol -> - val item = classifierSymbolToCompletionItem(symbol, partial) - if (item != null) { - to += item - } - } + to += toCompletionItems(compositeScope.callables { name -> matchesPrefix(name, partial) }, partial) + to += toCompletionItems(compositeScope.classifiers { name -> matchesPrefix(name, partial) }, partial) } +@JvmName("callablesToCompletionItems") +private fun KaSession.toCompletionItems(callables: Sequence, partial: String): Sequence = + callables.mapNotNull { + callableSymbolToCompletionItem(it, partial) + } + +@JvmName("classifiersToCompletionItems") +private fun KaSession.toCompletionItems(classifiers: Sequence, partial: String): Sequence = + classifiers.mapNotNull { + classifierSymbolToCompletionItem(it, partial) + } + private fun determineCompletionContext(element: PsiElement): CompletionContext { // Walk up to find a qualified expression where we're the selector val dotExpr = element.getParentOfType(strict = false) From 4129a47eb43f864ee236d9811377e2e9eaafc0cd Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 2 Apr 2026 19:21:30 +0530 Subject: [PATCH 23/58] feat: suggest local and imported extension functions Signed-off-by: Akash Yadav --- .../kotlin/completion/KotlinCompletions.kt | 144 ++++++++++++++---- 1 file changed, 112 insertions(+), 32 deletions(-) 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 07824fceb7..d29bd1e5bd 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 @@ -16,6 +16,7 @@ import org.jetbrains.kotlin.analysis.api.analyzeCopy import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode import org.jetbrains.kotlin.analysis.api.renderer.types.KaTypeRenderer import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource +import org.jetbrains.kotlin.analysis.api.scopes.KaScope import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol @@ -31,13 +32,13 @@ import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaTypeParameterSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol 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.com.intellij.psi.PsiElement import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtElement -import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType @@ -92,21 +93,48 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, ) { val completionContext = determineCompletionContext(elementAtOffset) + + // Find the nearest KtElement parent for scope resolution + val ktElement = elementAtOffset.getParentOfType(strict = false) + val scopeContext = ktElement?.let { element -> completionKtFile.scopeContext(element) } + val compositeScope = scopeContext?.compositeScope() val items = mutableListOf() - when (completionContext) { - CompletionContext.Scope -> collectScopeCompletions( - element = elementAtOffset, - file = completionKtFile, - partial = partial, - to = items + if (ktElement == null) { + logger.error( + "Cannot find parent of element {} with partial {}", + elementAtOffset, + partial ) - CompletionContext.Member -> collectMemberCompletions( - element = elementAtOffset, - partial = partial, - to = items + return@analyzeCopy CompletionResult.EMPTY + } + + if (compositeScope == null) { + logger.error( + "Unable to get CompositeScope for element {} with partial {}", + compositeScope, + partial ) + return@analyzeCopy CompletionResult.EMPTY + } + + when (completionContext) { + CompletionContext.Scope -> + collectScopeCompletions( + ktElement = ktElement, + scope = compositeScope, + partial = partial, + to = items + ) + + CompletionContext.Member -> + collectMemberCompletions( + scope = compositeScope, + element = elementAtOffset, + partial = partial, + to = items + ) } CompletionResult(items) @@ -122,6 +150,7 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult } private fun KaSession.collectMemberCompletions( + scope: KaScope, element: PsiElement, partial: String, to: MutableList @@ -140,12 +169,22 @@ private fun KaSession.collectMemberCompletions( return } + logger.info( + "Complete members of {}: {} [{}] matching '{}'", + receiver, + receiverType, + receiver.text, + partial + ) + collectMembersFromType(receiverType, partial, to) if (qualifiedExpr is KtSafeQualifiedExpression) { val nonNullType = receiverType.withNullability(isMarkedNullable = false) collectMembersFromType(nonNullType, partial, to) } + + collectExtensionFunctions(scope, partial, receiverType, to) } @OptIn(KaExperimentalApi::class) @@ -156,8 +195,15 @@ private fun KaSession.collectMembersFromType( ) { val typeScope = receiverType.scope if (typeScope != null) { - to += toCompletionItems(typeScope.getCallableSignatures { name -> matchesPrefix(name, partial) }.map { it.symbol }, partial) - to += toCompletionItems(typeScope.getClassifierSymbols { name -> matchesPrefix(name, partial) }, partial) + val callables = + typeScope.getCallableSignatures { name -> matchesPrefix(name, partial) } + .map { it.symbol } + + val classifiers = + typeScope.getClassifierSymbols { name -> matchesPrefix(name, partial) } + + to += toCompletionItems(callables, partial) + to += toCompletionItems(classifiers, partial) return } @@ -166,22 +212,37 @@ private fun KaSession.collectMembersFromType( val classSymbol = classType.symbol as? KaClassSymbol ?: return val memberScope = classSymbol.memberScope - to += toCompletionItems(memberScope.callables { name -> matchesPrefix(name, partial) }, partial) - to += toCompletionItems(memberScope.classifiers { name -> matchesPrefix(name, partial) }, partial) + val callables = memberScope.callables { name -> matchesPrefix(name, partial) } + val classifiers = memberScope.classifiers { name -> matchesPrefix(name, partial) } + + to += toCompletionItems(callables, partial) + to += toCompletionItems(classifiers, partial) } -private fun KaSession.collectScopeCompletions( - element: PsiElement, - file: KtFile, +private fun KaSession.collectExtensionFunctions( + scope: KaScope, partial: String, + receiverType: KaType, to: MutableList ) { - // Find the nearest KtElement parent for scope resolution - val ktElement = element.getParentOfType(strict = false) - if (ktElement == null) { - logger.error("Cannot find parent of element {} with partial {}", element, partial) - return - } + val extensionSymbols = + scope.callables { name -> matchesPrefix(name, partial) } + .filter { symbol -> + if (!symbol.isExtension) return@filter false + + val extReceiverType = symbol.receiverType ?: return@filter false + receiverType.isSubtypeOf(extReceiverType) + } + + to += toCompletionItems(extensionSymbols, partial) +} + +private fun KaSession.collectScopeCompletions( + ktElement: KtElement, + scope: KaScope, + partial: String, + to: MutableList, +) { logger.info( "Complete scope members of {}: [{}] matching '{}'", @@ -190,21 +251,30 @@ private fun KaSession.collectScopeCompletions( partial ) - val scopeContext = file.scopeContext(ktElement) - val compositeScope = scopeContext.compositeScope() - - to += toCompletionItems(compositeScope.callables { name -> matchesPrefix(name, partial) }, partial) - to += toCompletionItems(compositeScope.classifiers { name -> matchesPrefix(name, partial) }, partial) + to += toCompletionItems( + scope.callables { name -> matchesPrefix(name, partial) }, + partial + ) + to += toCompletionItems( + scope.classifiers { name -> matchesPrefix(name, partial) }, + partial + ) } @JvmName("callablesToCompletionItems") -private fun KaSession.toCompletionItems(callables: Sequence, partial: String): Sequence = +private fun KaSession.toCompletionItems( + callables: Sequence, + partial: String +): Sequence = callables.mapNotNull { callableSymbolToCompletionItem(it, partial) } @JvmName("classifiersToCompletionItems") -private fun KaSession.toCompletionItems(classifiers: Sequence, partial: String): Sequence = +private fun KaSession.toCompletionItems( + classifiers: Sequence, + partial: String +): Sequence = classifiers.mapNotNull { classifierSymbolToCompletionItem(it, partial) } @@ -284,7 +354,11 @@ private fun KaSession.classifierSymbolToCompletionItem( val item = createSymbolCompletionItem(symbol, partial) ?: return null item.detail = when (symbol) { is KaClassSymbol -> symbol.classId?.asFqNameString() ?: "" - is KaTypeAliasSymbol -> renderName(symbol.expandedType, KaTypeRendererForSource.WITH_QUALIFIED_NAMES) + is KaTypeAliasSymbol -> renderName( + symbol.expandedType, + KaTypeRendererForSource.WITH_QUALIFIED_NAMES + ) + is KaTypeParameterSymbol -> item.ideLabel } return item @@ -347,6 +421,12 @@ private fun partialIdentifier(prefix: String): String { } private fun matchesPrefix(name: Name, partial: String): Boolean { + logger.info( + "'{}' matches '{}': {}", + name, + partial, + name.asString().startsWith(partial, ignoreCase = true) + ) if (partial.isEmpty()) return true return name.asString().startsWith(partial, ignoreCase = true) } From 7d736fafd995c202b2a5389a1e6bfebaaf207b9f Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 2 Apr 2026 19:30:32 +0530 Subject: [PATCH 24/58] fix: do not suggest extension functions for scope completions This ensures that extension functions whose receiver type is not available in the current scope are not suggested for scope completions Signed-off-by: Akash Yadav --- .../kotlin/completion/KotlinCompletions.kt | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) 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 d29bd1e5bd..4d868d382b 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 @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.analysis.api.KaContextParameterApi import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.KaSession import org.jetbrains.kotlin.analysis.api.analyzeCopy +import org.jetbrains.kotlin.analysis.api.components.KaScopeContext import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode import org.jetbrains.kotlin.analysis.api.renderer.types.KaTypeRenderer import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource @@ -122,8 +123,9 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult when (completionContext) { CompletionContext.Scope -> collectScopeCompletions( - ktElement = ktElement, + scopeContext = scopeContext, scope = compositeScope, + ktElement = ktElement, partial = partial, to = items ) @@ -238,12 +240,12 @@ private fun KaSession.collectExtensionFunctions( } private fun KaSession.collectScopeCompletions( - ktElement: KtElement, + scopeContext: KaScopeContext, scope: KaScope, + ktElement: KtElement, partial: String, to: MutableList, ) { - logger.info( "Complete scope members of {}: [{}] matching '{}'", ktElement, @@ -251,14 +253,23 @@ private fun KaSession.collectScopeCompletions( partial ) - to += toCompletionItems( - scope.callables { name -> matchesPrefix(name, partial) }, - partial - ) - to += toCompletionItems( - scope.classifiers { name -> matchesPrefix(name, partial) }, - partial - ) + val callables = + scope.callables { name -> matchesPrefix(name, partial) } + .filter { symbol -> + + // always include non-extension functions + if (!symbol.isExtension) return@filter true + + // include extension functions with matching implicit receivers + val extReceiverType = symbol.receiverType ?: return@filter true + scopeContext.implicitReceivers.any { receiver -> + receiver.type.isSubtypeOf(extReceiverType) + } + } + val classifiers = scope.classifiers { name -> matchesPrefix(name, partial) } + + to += toCompletionItems(callables, partial) + to += toCompletionItems(classifiers, partial) } @JvmName("callablesToCompletionItems") From 87234f8331530425ede530f7e50ccae62440edf9 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 3 Apr 2026 16:37:35 +0530 Subject: [PATCH 25/58] feat: add scope-sensitive keyword completions Signed-off-by: Akash Yadav --- .../lsp/kotlin/completion/ContextKeywords.kt | 53 ++++++ .../lsp/kotlin/completion/ContextResolver.kt | 166 +++++++++++++++++ .../kotlin/completion/DeclarationContext.kt | 25 +++ .../kotlin/completion/KotlinCompletions.kt | 119 ++++++------ .../lsp/kotlin/completion/ModifierFilter.kt | 174 ++++++++++++++++++ 5 files changed, 481 insertions(+), 56 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt new file mode 100644 index 0000000000..433963f83e --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt @@ -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 = 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 + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt new file mode 100644 index 0000000000..3878cb57ac --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt @@ -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, + 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(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(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(strict = false) + if (dotExpr != null && isInSelectorPosition(element, dotExpr)) { + return CompletionContext.Member + } + + val safeExpr = element.getParentOfType(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()) { + 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 + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt new file mode 100644 index 0000000000..27f3de4703 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt @@ -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 +} 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 4d868d382b..51613960b2 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 @@ -80,45 +80,37 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult append(originalText, completionOffset, originalText.length) } - val completionKtFile = managedFile.createInMemoryFileWithContent(parser, textWithPlaceholder) - val elementAtOffset = completionKtFile.findElementAt(completionOffset) - - if (elementAtOffset == null) { - logger.error("Unable to locate element at position {}", requestPosition) - return CompletionResult.EMPTY - } + val completionKtFile = + managedFile.createInMemoryFileWithContent( + psiFactory = parser, + content = textWithPlaceholder + ) return try { analyzeCopy( useSiteElement = completionKtFile, resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, ) { - val completionContext = determineCompletionContext(elementAtOffset) - - // Find the nearest KtElement parent for scope resolution - val ktElement = elementAtOffset.getParentOfType(strict = false) - val scopeContext = ktElement?.let { element -> completionKtFile.scopeContext(element) } - val compositeScope = scopeContext?.compositeScope() - val items = mutableListOf() - - if (ktElement == null) { + val cursorContext = resolveCursorContext(completionKtFile, completionOffset) + if (cursorContext == null) { logger.error( - "Cannot find parent of element {} with partial {}", - elementAtOffset, - partial + "Unable to determine context at offset {} in file {}", + completionOffset, + params.file ) - return@analyzeCopy CompletionResult.EMPTY } - if (compositeScope == null) { - logger.error( - "Unable to get CompositeScope for element {} with partial {}", - compositeScope, - partial - ) - return@analyzeCopy CompletionResult.EMPTY - } + val ( + psiElement, + _, + ktElement, + scopeContext, + compositeScope, + completionContext + ) = cursorContext + + val items = mutableListOf() when (completionContext) { CompletionContext.Scope -> @@ -133,12 +125,18 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult CompletionContext.Member -> collectMemberCompletions( scope = compositeScope, - element = elementAtOffset, + element = psiElement, partial = partial, to = items ) } + collectKeywordCompletions( + ctx = cursorContext, + partial = partial, + to = items + ) + CompletionResult(items) } } catch (e: Throwable) { @@ -272,6 +270,29 @@ private fun KaSession.collectScopeCompletions( to += toCompletionItems(classifiers, partial) } +private fun KaSession.collectKeywordCompletions( + ctx: CursorContext, + partial: String, + to: MutableList, +) { + fun kwItem(name: String) = + ktCompletionItem( + name = name, + kind = CompletionItemKind.KEYWORD, + partial = partial + ) + + if (!ctx.isInsideModifierList) { + ContextKeywords.keywordsFor(ctx.declarationContext).mapTo(to) { kw -> + kwItem(kw.value) + } + } + + ModifierFilter.validModifiers(ctx).mapTo(to) { kw -> + kwItem(kw.value) + } +} + @JvmName("callablesToCompletionItems") private fun KaSession.toCompletionItems( callables: Sequence, @@ -290,30 +311,6 @@ private fun KaSession.toCompletionItems( classifierSymbolToCompletionItem(it, partial) } -private fun determineCompletionContext(element: PsiElement): CompletionContext { - // Walk up to find a qualified expression where we're the selector - val dotExpr = element.getParentOfType(strict = false) - if (dotExpr != null && isInSelectorPosition(element, dotExpr)) { - return CompletionContext.Member - } - - val safeExpr = element.getParentOfType(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 -} - @OptIn(KaExperimentalApi::class) private fun KaSession.callableSymbolToCompletionItem( symbol: KaCallableSymbol, @@ -379,12 +376,22 @@ private fun KaSession.createSymbolCompletionItem( symbol: KaSymbol, partial: String ): CompletionItem? { - val name = symbol.name?.asString() ?: return null + return ktCompletionItem( + name = symbol.name?.asString() ?: return null, + kind = kindOf(symbol), + partial = partial, + ) +} +private fun KaSession.ktCompletionItem( + name: String, + kind: CompletionItemKind, + partial: String, +): CompletionItem { val item = KotlinCompletionItem() item.ideLabel = name - item.completionKind = kindOf(symbol) - item.matchLevel = CompletionItem.matchLevel(name, partial) + item.completionKind = kind + item.matchLevel = CompletionItem.matchLevel(item.ideLabel, partial) return item } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt new file mode 100644 index 0000000000..5f0710e1d8 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt @@ -0,0 +1,174 @@ +package com.itsaky.androidide.lsp.kotlin.completion + +import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet +import org.jetbrains.kotlin.lexer.KtModifierKeywordToken +import org.jetbrains.kotlin.lexer.KtTokens.* + +/** + * Helper for filtering modifier keywords for keyword completions. + */ +object ModifierFilter { + + /** + * Returns which modifier keywords are valid to suggest given the + * current context, declaration kind, and already-present modifiers. + */ + fun validModifiers( + ctx: CursorContext, + ): Set { + val (_, _, _, _, _, _, declCtx, declKind, existing, _) = ctx + + val candidates = MODIFIER_KEYWORDS_ARRAY.toMutableSet() + candidates -= existing + + // remove mutually exclusive groups + if (VISIBILITY_MODIFIERS.types.any { it in existing }) + candidates -= VISIBILITY_MODIFIERS.types() + if (MODALITY_MODIFIERS.types.any { it in existing }) + candidates -= MODALITY_MODIFIERS.types() + + when (declCtx) { + DeclarationContext.INTERFACE_BODY -> { + // interface members are open by default; sealed/final don't apply to members + candidates -= setOf(FINAL_KEYWORD, OPEN_KEYWORD, SEALED_KEYWORD) + + // inner classes not allowed in interfaces + candidates -= INNER_KEYWORD + } + + DeclarationContext.FUNCTION_BODY -> { + // local declarations: only a small subset of modifiers are legal + candidates.retainAll( + setOf( + INLINE_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, + SUSPEND_KEYWORD, TAILREC_KEYWORD, + DATA_KEYWORD, // local data class (Kotlin 1.9+) + INNER_KEYWORD, + ) + ) + } + + DeclarationContext.OBJECT_BODY, + DeclarationContext.TOP_LEVEL, + DeclarationContext.SCRIPT_TOP_LEVEL -> candidates -= INNER_KEYWORD // inner only valid inside a class + + else -> Unit + } + + when (declKind) { + DeclarationKind.PROPERTY_VAL -> { + candidates -= setOf( + LATEINIT_KEYWORD, // lateinit requires var + VARARG_KEYWORD, + NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, + INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, + ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, + VALUE_KEYWORD, + ) + + // const only on top-level or companion object val + if (declCtx !in setOf( + DeclarationContext.TOP_LEVEL, + DeclarationContext.OBJECT_BODY, + DeclarationContext.SCRIPT_TOP_LEVEL + ) + ) + candidates -= CONST_KEYWORD + } + + DeclarationKind.PROPERTY_VAR -> { + candidates -= setOf( + CONST_KEYWORD, // const requires val + VARARG_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, + INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, + ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, + VALUE_KEYWORD, + ) + } + + DeclarationKind.FUN -> { + candidates -= setOf( + LATEINIT_KEYWORD, CONST_KEYWORD, VARARG_KEYWORD, + INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, + ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, + VALUE_KEYWORD, + ) + // abstract fun can't be inline/tailrec/external simultaneously + if (ABSTRACT_KEYWORD in existing) { + candidates -= setOf(INLINE_KEYWORD, TAILREC_KEYWORD, EXTERNAL_KEYWORD) + } + } + + DeclarationKind.CLASS -> { + candidates -= setOf( + LATEINIT_KEYWORD, CONST_KEYWORD, + VARARG_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, + REIFIED_KEYWORD, + ) + + // sealed is a modality modifier and conflicts with open/final/abstract + if (SEALED_KEYWORD in existing) + candidates -= setOf(OPEN_KEYWORD, FINAL_KEYWORD, ABSTRACT_KEYWORD) + + // value class requires @JvmInline in practice, but `value` keyword is valid + } + + DeclarationKind.INTERFACE -> { + // interfaces are implicitly abstract; most modifiers don't apply + candidates.retainAll( + setOf( + PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD, + EXPECT_KEYWORD, ACTUAL_KEYWORD, + SEALED_KEYWORD, // sealed interface + EXTERNAL_KEYWORD, FUN_KEYWORD, // fun interface + ) + ) + } + + DeclarationKind.OBJECT -> { + candidates.retainAll( + setOf( + PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD, + EXPECT_KEYWORD, ACTUAL_KEYWORD, EXTERNAL_KEYWORD, + DATA_KEYWORD, // data object (Kotlin 1.9+) + COMPANION_KEYWORD, + ) + ) + } + + DeclarationKind.CONSTRUCTOR -> { + // constructors only take visibility modifiers + candidates.retainAll(VISIBILITY_MODIFIERS.types()) + } + + DeclarationKind.UNKNOWN -> { + // Cursor is after some modifiers but before any keyword. + // Keep all modifiers that are valid given what's already typed; + // the exclusion rules above already handled mutual exclusions. + } + + else -> Unit + } + + // expect and actual are mutually exclusive + if (EXPECT_KEYWORD in existing) candidates -= ACTUAL_KEYWORD + if (ACTUAL_KEYWORD in existing) candidates -= EXPECT_KEYWORD + + // noinline, crossinline and reified keywords are invalid if the + // function is not inline + if (INLINE_KEYWORD !in existing) { + candidates -= setOf(NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, REIFIED_KEYWORD) + } + + return candidates + } + + private fun TokenSet.types(): Set = + types.filterIsInstance().toSet() + + private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = + this.contains(token) +} \ No newline at end of file From 7d78db1c62bfd24d9b1b301e781a0d0b0f1496ba Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Mon, 6 Apr 2026 22:52:04 +0530 Subject: [PATCH 26/58] feat: add indexing api and service implementation Signed-off-by: Akash Yadav --- .../services/builder/GradleBuildService.kt | 6 + gradle/libs.versions.toml | 8 +- lsp/indexing/build.gradle.kts | 19 + .../codeonthego/indexing/FilteredIndex.kt | 112 +++++ .../codeonthego/indexing/InMemoryIndex.kt | 226 +++++++++ .../codeonthego/indexing/MergedIndex.kt | 82 ++++ .../codeonthego/indexing/PersistentIndex.kt | 336 ++++++++++++++ .../codeonthego/indexing/api/Core.kt | 91 ++++ .../codeonthego/indexing/api/Index.kt | 105 +++++ .../codeonthego/indexing/api/Query.kt | 78 ++++ .../indexing/service/IndexRegistry.kt | 119 +++++ .../indexing/service/IndexingService.kt | 41 ++ .../service/IndexingServiceManager.kt | 158 +++++++ .../indexing/util/BackgroundIndexer.kt | 214 +++++++++ lsp/java/build.gradle.kts | 1 + .../androidide/lsp/java/JavaLanguageServer.kt | 19 +- lsp/jvm-symbol-index/build.gradle.kts | 24 + .../indexing/jvm/CombinedJarScanner.kt | 78 ++++ .../indexing/jvm/JarSymbolScanner.kt | 305 +++++++++++++ .../indexing/jvm/JvmIndexingService.kt | 149 ++++++ .../codeonthego/indexing/jvm/JvmSymbol.kt | 204 +++++++++ .../indexing/jvm/JvmSymbolDescriptor.kt | 409 +++++++++++++++++ .../indexing/jvm/JvmSymbolIndex.kt | 191 ++++++++ .../indexing/jvm/KotlinMetadataScanner.kt | 428 ++++++++++++++++++ lsp/jvm-symbol-models/build.gradle.kts | 37 ++ .../src/main/proto/jvm_symbol.proto | 210 +++++++++ lsp/kotlin/build.gradle.kts | 1 + .../lsp/kotlin/KotlinLanguageServer.kt | 7 + settings.gradle.kts | 3 + subprojects/projects/build.gradle.kts | 1 + .../androidide/projects/ProjectManagerImpl.kt | 22 +- 31 files changed, 3677 insertions(+), 7 deletions(-) create mode 100644 lsp/indexing/build.gradle.kts create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndex.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndex.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/MergedIndex.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/PersistentIndex.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Core.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Index.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Query.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexRegistry.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingService.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt create mode 100644 lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt create mode 100644 lsp/jvm-symbol-index/build.gradle.kts create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmIndexingService.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbol.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt create mode 100644 lsp/jvm-symbol-models/build.gradle.kts create mode 100644 lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt index 85f95e5324..d92830bbfb 100644 --- a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt +++ b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.kt @@ -449,6 +449,12 @@ class GradleBuildService : ), ) + buildServiceScope.launch { + ProjectManagerImpl.getInstance() + .indexingServiceManager + .onBuildCompleted() + } + EventBus.getDefault() .post( BuildCompletedEvent( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dff1dee11d..b9e0b63487 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ activityKtx = "1.8.2" agp = "8.8.2" agp-tooling = "8.11.0" -appcompat = "1.6.1" +androidx-sqlite = "2.6.2" appcompatVersion = "1.7.1" bcprovJdk18on = "1.80" colorpickerview = "2.3.0" @@ -94,6 +94,8 @@ androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtxVersion" } androidx-recyclerview-v132 = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" } +androidx-sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "androidx-sqlite" } +androidx-sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "androidx-sqlite" } androidx-viewpager2-v110beta02 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcprovJdk18on" } bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprovJdk18on" } @@ -118,10 +120,8 @@ desugar_jdk_libs-v215 = { module = "com.android.tools:desugar_jdk_libs", version google-genai = { module = "com.google.genai:google-genai", version.ref = "googleGenai" } gson-v2101 = { module = "com.google.code.gson:gson", version.ref = "gson" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -play-services-oss-licenses = { module = "com.google.android.gms:play-services-oss-licenses", version.ref = "playServicesOssLicenses" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } @@ -307,6 +307,8 @@ androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "m monitor = { group = "androidx.test", name = "monitor", version.ref = "monitorVersion" } org-json = { module = "org.json:json", version = "20210307"} +kotlinx-metadata = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/lsp/indexing/build.gradle.kts b/lsp/indexing/build.gradle.kts new file mode 100644 index 0000000000..b81c2d47b3 --- /dev/null +++ b/lsp/indexing/build.gradle.kts @@ -0,0 +1,19 @@ +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.lsp.indexing" +} + +dependencies { + api(libs.androidx.annotation) + api(libs.androidx.sqlite.ktx) + api(libs.androidx.sqlite.framework) + api(libs.kotlinx.coroutines.core) + + api(projects.logger) +} 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 new file mode 100644 index 0000000000..d5a8527ca3 --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndex.kt @@ -0,0 +1,112 @@ +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 +import java.io.Closeable +import java.util.concurrent.ConcurrentHashMap + +/** + * A read-only view over an index that only exposes entries + * from a set of active sources. + * + * The underlying index retains ALL data (it's a persistent cache). + * This view controls which subset is visible based on which + * sources (JAR paths, etc.) are currently "active." + * + * @param T The indexed type. + * @param backing The underlying index that holds all data. + */ +open class FilteredIndex( + private val backing: ReadableIndex, +) : ReadableIndex, Closeable { + + /** + * The set of source IDs whose entries are visible. + * Uses a concurrent set for thread-safe reads during queries. + */ + private val activeSources = ConcurrentHashMap.newKeySet() + + /** + * Make a source's entries visible in query results. + */ + fun activateSource(sourceId: String) { + activeSources.add(sourceId) + } + + /** + * Hide a source's entries from query results. + * The data remains in the backing index. + */ + fun deactivateSource(sourceId: String) { + activeSources.remove(sourceId) + } + + /** + * Replace the entire active set. Sources not in [sourceIds] + * become invisible; sources in [sourceIds] become visible. + * + * This is the typical call on project sync: pass in all + * current classpath JAR paths. + */ + fun setActiveSources(sourceIds: Set) { + activeSources.clear() + activeSources.addAll(sourceIds) + } + + /** + * Returns the current set of active source IDs. + */ + fun activeSources(): Set = + activeSources.toSet() + + /** + * Returns true if the source is currently active (visible). + */ + fun isActive(sourceId: String): Boolean = + sourceId in activeSources + + /** + * Returns true if the source exists in the backing index, + * regardless of whether it's active. + * + * Use this to check if a JAR needs indexing at all. + */ + 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 + if (query.sourceId != null && query.sourceId !in activeSources) { + return kotlinx.coroutines.flow.emptyFlow() + } + + return backing.query(query).filter { it.sourceId in activeSources } + } + + override suspend fun get(key: String): T? { + val entry = backing.get(key) ?: return null + return if (entry.sourceId in activeSources) entry else null + } + + override suspend fun containsSource(sourceId: String): Boolean { + return sourceId in activeSources && backing.containsSource(sourceId) + } + + override fun distinctValues(fieldName: String): Flow { + // 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 + // (the main use case), this approximation is acceptable + // since packages from inactive JARs are harmless — they + // just produce empty results when queried further. + return backing.distinctValues(fieldName) + } + + override fun close() { + 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 new file mode 100644 index 0000000000..20067e998e --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndex.kt @@ -0,0 +1,226 @@ +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 +import org.appdevforall.codeonthego.indexing.api.Indexable +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.collections.iterator +import kotlin.concurrent.read +import kotlin.concurrent.write + +/** + * A thread-safe, in-memory [Index] backed by [ConcurrentHashMap]. + * + * Optimized for small-to-medium datasets (source files, typically + * hundreds to low thousands of entries) that change frequently. + * + * Data layout: + * - [primaryMap]: key → entry (O(1) point lookup) + * - [sourceMap]: sourceId → set of keys (O(1) bulk removal) + * - [fieldMaps]: fieldName → (fieldValue → set of keys) (equality filter) + * - [prefixBuckets]: fieldName → (lowercased first char → list of (value, key)) + * Provides a ~36-way partition for prefix search. + * + * All mutations go through [lock] in write mode for consistency + * across the multiple maps. Reads use read mode. + * + * @param T The indexed entry type. + * @param descriptor Defines queryable fields and serialization. + */ +class InMemoryIndex( + override val descriptor: IndexDescriptor, + override val name: String = "memory:${descriptor.name}", +) : Index { + + private val primaryMap = ConcurrentHashMap(256) + private val sourceMap = ConcurrentHashMap>(32) + private val fieldMaps = ConcurrentHashMap>>() + private val prefixBuckets = ConcurrentHashMap>>() + + private val lock = ReentrantReadWriteLock() + + private data class PrefixEntry(val lowerValue: String, val key: String) + + init { + for (field in descriptor.fields) { + fieldMaps[field.name] = ConcurrentHashMap() + if (field.prefixSearchable) { + prefixBuckets[field.name] = ConcurrentHashMap() + } + } + } + + override fun query(query: IndexQuery): Flow = flow { + 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++ + } + } + + 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 = 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 suspend fun insertAll(entries: Sequence) { + lock.write { + for (entry in entries) { + insertSingleLocked(entry) + } + } + } + + override suspend fun insert(entry: T) = insertSingle(entry) + + override suspend fun removeBySource(sourceId: String) = lock.write { + val keys = sourceMap.remove(sourceId) ?: return@write + for (key in keys) { + val entry = primaryMap.remove(key) ?: continue + removeFromSecondaryIndexes(entry) + } + } + + override suspend fun clear() = lock.write { + primaryMap.clear() + sourceMap.clear() + fieldMaps.values.forEach { it.clear() } + prefixBuckets.values.forEach { it.clear() } + } + + val size: Int get() = primaryMap.size + val sourceCount: Int get() = sourceMap.size + + /** + * Resolves the set of keys matching the query by intersecting + * the results of each predicate. + * + * Starts with the most selective predicate to minimize the + * intersection set. + */ + private fun resolveMatchingKeys(query: IndexQuery): Sequence = lock.read { + var candidates: Set? = null + + if (query.key != null) { + return@read if (primaryMap.containsKey(query.key)) { + sequenceOf(query.key) + } else { + emptySequence() + } + } + + if (query.sourceId != null) { + candidates = intersect(candidates, sourceMap[query.sourceId]) + } + + for ((field, value) in query.exactMatch) { + val fieldMap = fieldMaps[field] ?: return@read emptySequence() + candidates = intersect(candidates, fieldMap[value]) + } + + for ((field, prefix) in query.prefixMatch) { + val buckets = prefixBuckets[field] ?: return@read emptySequence() + val lowerPrefix = prefix.lowercase() + val firstChar = lowerPrefix.firstOrNull() ?: continue + val bucket = buckets[firstChar] ?: return@read emptySequence() + + val matching = bucket.asSequence() + .filter { it.lowerValue.startsWith(lowerPrefix) } + .map { it.key } + .toSet() + + candidates = intersect(candidates, matching) + } + + for ((field, mustExist) in query.presence) { + val fieldMap = fieldMaps[field] ?: return@read emptySequence() + val allKeysWithField = fieldMap.values.flatMapTo(mutableSetOf()) { it } + candidates = if (mustExist) { + intersect(candidates, allKeysWithField) + } else { + // Keys that DON'T have this field + val allKeys = primaryMap.keys.toMutableSet() + allKeys.removeAll(allKeysWithField) + intersect(candidates, allKeys) + } + } + + candidates?.asSequence() ?: primaryMap.keys.asSequence() + } + + private fun intersect(current: Set?, other: Set?): Set? { + if (other == null) return current + if (current == null) return other + 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) { + removeFromSecondaryIndexes(existing) + } + + primaryMap[entry.key] = entry + sourceMap.getOrPut(entry.sourceId) { mutableSetOf() }.add(entry.key) + + val fields = descriptor.fieldValues(entry) + for ((fieldName, value) in fields) { + if (value == null) continue + + fieldMaps[fieldName] + ?.getOrPut(value) { mutableSetOf() } + ?.add(entry.key) + + val buckets = prefixBuckets[fieldName] + if (buckets != null) { + val lower = value.lowercase() + val firstChar = lower.firstOrNull() ?: continue + buckets.getOrPut(firstChar) { mutableListOf() } + .add(PrefixEntry(lower, entry.key)) + } + } + } + + private fun removeFromSecondaryIndexes(entry: T) { + val fields = descriptor.fieldValues(entry) + for ((fieldName, value) in fields) { + if (value == null) continue + + fieldMaps[fieldName]?.get(value)?.remove(entry.key) + + val buckets = prefixBuckets[fieldName] + if (buckets != null) { + val lower = value.lowercase() + val firstChar = lower.firstOrNull() ?: continue + buckets[firstChar]?.removeAll { it.key == entry.key } + } + } + // Note: sourceMap is handled by the caller + } +} 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 new file mode 100644 index 0000000000..af39930033 --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/MergedIndex.kt @@ -0,0 +1,82 @@ +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. + * + * @param T The indexed type. + * @param indexes The indexes to merge, in priority order. + */ +class MergedIndex( + private val indexes: List>, +) : ReadableIndex, Closeable { + + constructor(vararg indexes: ReadableIndex) : this(indexes.toList()) + + override fun query(query: IndexQuery): Flow = channelFlow { + val seen = ConcurrentHashMap.newKeySet() + 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. + 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 + } + } + } + } + } + } + + override suspend fun get(key: String): T? { + // First match wins (priority order) + for (index in indexes) { + val result = index.get(key) + if (result != null) return result + } + return null + } + + override suspend fun containsSource(sourceId: String): Boolean { + return indexes.any { it.containsSource(sourceId) } + } + + override fun distinctValues(fieldName: String): Flow = channelFlow { + val seen = ConcurrentHashMap.newKeySet() + for (index in indexes) { + launch { + index.distinctValues(fieldName).collect { value -> + if (seen.add(value)) { + send(value) + } + } + } + } + } + + override fun close() { + for (index in indexes) { + if (index is Closeable) index.close() + } + } +} diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/PersistentIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/PersistentIndex.kt new file mode 100644 index 0000000000..f3b0cf539b --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/PersistentIndex.kt @@ -0,0 +1,336 @@ +package org.appdevforall.codeonthego.indexing + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +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 +import org.appdevforall.codeonthego.indexing.api.IndexQuery +import org.appdevforall.codeonthego.indexing.api.Indexable +import kotlin.collections.iterator + +/** + * A persistent [Index] backed by SQLite via AndroidX. + * + * Creates a table dynamically based on the [IndexDescriptor]: + * ``` + * CREATE TABLE IF NOT EXISTS {name} ( + * _key TEXT PRIMARY KEY, + * _source_id TEXT NOT NULL, + * f_{field1} TEXT, + * f_{field1}_lower TEXT, -- if prefix-searchable + * f_{field2} TEXT, + * ... + * _payload BLOB NOT NULL + * ); + * ``` + * + * SQL indexes are created on: + * - `_source_id` (for bulk removal) + * - Each `f_{field}` (for equality filter) + * - Each `f_{field}_lower` (for prefix search via `LIKE 'prefix%'`) + * + * Uses WAL journal mode for concurrent read/write performance. + * Inserts are batched inside transactions for throughput. + * + * @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 + * a database (each gets its own table) or use separate files. + * @param batchSize Number of rows per INSERT transaction. + */ +class PersistentIndex( + override val descriptor: IndexDescriptor, + context: Context, + dbName: String, + override val name: String = "persistent:${descriptor.name}", + private val batchSize: Int = 500, +) : Index { + + private val tableName = descriptor.name.replace(Regex("[^a-zA-Z0-9_]"), "_") + + // Field column names: "f_{fieldName}" + private val fieldColumns = descriptor.fields.associate { field -> + field.name to "f_${field.name}" + } + + // Prefix-searchable fields also get a "_lower" column + private val prefixColumns = descriptor.fields + .filter { it.prefixSearchable } + .associate { it.name to "f_${it.name}_lower" } + + private val db: SupportSQLiteDatabase + + init { + val config = SupportSQLiteOpenHelper.Configuration.builder(context) + .name(dbName) + .callback(object : SupportSQLiteOpenHelper.Callback(1) { + override fun onCreate(db: SupportSQLiteDatabase) { + createTable(db) + } + + override fun onUpgrade( + db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int, + ) { + // TODO: Add migration support + db.execSQL("DROP TABLE IF EXISTS $tableName") + createTable(db) + } + + override fun onOpen(db: SupportSQLiteDatabase) { + } + }) + .build() + + db = FrameworkSQLiteOpenHelperFactory() + .create(config) + .writableDatabase + + // Ensure table exists (for shared databases) + createTable(db) + } + + override fun query(query: IndexQuery): Flow = flow { + val (sql, args) = buildSelectQuery(query) + val cursor = db.query(sql, args.toTypedArray()) + + cursor.use { + val payloadIdx = it.getColumnIndexOrThrow("_payload") + while (it.moveToNext()) { + val bytes = it.getBlob(payloadIdx) + emit(descriptor.deserialize(bytes)) + } + } + }.flowOn(Dispatchers.IO) + + override suspend fun get(key: String): T? = withContext(Dispatchers.IO) { + val cursor = db.query( + "SELECT _payload FROM $tableName WHERE _key = ? LIMIT 1", + arrayOf(key), + ) + cursor.use { + if (it.moveToFirst()) { + descriptor.deserialize(it.getBlob(0)) + } else null + } + } + + override suspend fun containsSource(sourceId: String): Boolean = + withContext(Dispatchers.IO) { + val cursor = db.query( + "SELECT 1 FROM $tableName WHERE _source_id = ? LIMIT 1", + arrayOf(sourceId), + ) + cursor.use { it.moveToFirst() } + } + + override fun distinctValues(fieldName: String): Flow = flow { + 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() + } + } + // Flush remaining + if (batch.isNotEmpty()) { + insertBatch(batch) + } + } + + override suspend fun insertAll(entries: Sequence) = withContext(Dispatchers.IO) { + val batch = mutableListOf() + for (entry in entries) { + batch.add(entry) + if (batch.size >= batchSize) { + insertBatch(batch) + batch.clear() + } + } + if (batch.isNotEmpty()) { + insertBatch(batch) + } + } + + override suspend fun insert(entry: T) = withContext(Dispatchers.IO) { + insertBatch(listOf(entry)) + } + + override suspend fun removeBySource(sourceId: String) = withContext(Dispatchers.IO) { + db.execSQL("DELETE FROM $tableName WHERE _source_id = ?", arrayOf(sourceId)) + } + + override suspend fun clear() = withContext(Dispatchers.IO) { + db.execSQL("DELETE FROM $tableName") + } + + override fun close() { + db.close() + } + + suspend fun size(): Int = withContext(Dispatchers.IO) { + val cursor = db.query("SELECT COUNT(*) FROM $tableName") + cursor.use { if (it.moveToFirst()) it.getInt(0) else 0 } + } + + private fun createTable(db: SupportSQLiteDatabase) { + val columns = buildString { + append("_key TEXT PRIMARY KEY, ") + append("_source_id TEXT NOT NULL, ") + + for (field in descriptor.fields) { + val col = fieldColumns[field.name]!! + append("$col TEXT, ") + + if (field.prefixSearchable) { + val lowerCol = prefixColumns[field.name]!! + append("$lowerCol TEXT, ") + } + } + + append("_payload BLOB NOT NULL") + } + + db.execSQL("CREATE TABLE IF NOT EXISTS $tableName ($columns)") + + // Indexes + db.execSQL( + "CREATE INDEX IF NOT EXISTS idx_${tableName}_source ON $tableName(_source_id)" + ) + + for (field in descriptor.fields) { + val col = fieldColumns[field.name]!! + db.execSQL( + "CREATE INDEX IF NOT EXISTS idx_${tableName}_$col ON $tableName($col)" + ) + + if (field.prefixSearchable) { + val lowerCol = prefixColumns[field.name]!! + db.execSQL( + "CREATE INDEX IF NOT EXISTS idx_${tableName}_$lowerCol ON $tableName($lowerCol)" + ) + } + } + } + + private fun insertBatch(entries: List) { + db.beginTransaction() + try { + for (entry in entries) { + val cv = ContentValues().apply { + put("_key", entry.key) + put("_source_id", entry.sourceId) + + val fields = descriptor.fieldValues(entry) + for ((fieldName, value) in fields) { + val col = fieldColumns[fieldName] ?: continue + put(col, value) + + val lowerCol = prefixColumns[fieldName] + if (lowerCol != null) { + put(lowerCol, value?.lowercase()) + } + } + + put("_payload", descriptor.serialize(entry)) + } + + db.insert( + tableName, + SQLiteDatabase.CONFLICT_REPLACE, + cv, + ) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private data class SqlQuery(val sql: String, val args: List) + + private fun buildSelectQuery(query: IndexQuery): SqlQuery { + val where = StringBuilder() + val args = mutableListOf() + + fun and(clause: String, vararg values: String) { + if (where.isNotEmpty()) where.append(" AND ") + where.append(clause) + args.addAll(values) + } + + query.key?.let { and("_key = ?", it) } + query.sourceId?.let { and("_source_id = ?", it) } + + for ((field, value) in query.exactMatch) { + val col = fieldColumns[field] ?: continue + and("$col = ?", value) + } + + for ((field, prefix) in query.prefixMatch) { + val lowerCol = prefixColumns[field] + if (lowerCol != null) { + // Use the pre-lowercased column for index-friendly LIKE + and("$lowerCol LIKE ?", "${prefix.lowercase()}%") + } else { + // Fallback: case-sensitive prefix on the regular column + val col = fieldColumns[field] ?: continue + and("$col LIKE ?", "$prefix%") + } + } + + for ((field, mustExist) in query.presence) { + val col = fieldColumns[field] ?: continue + if (mustExist) { + and("$col IS NOT NULL") + } else { + and("$col IS NULL") + } + } + + val sql = buildString { + append("SELECT _payload FROM $tableName") + if (where.isNotEmpty()) { + append(" WHERE ") + append(where) + } + if (query.limit > 0) { + append(" LIMIT ${query.limit}") + } + } + + return SqlQuery(sql, args) + } +} diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Core.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Core.kt new file mode 100644 index 0000000000..b595e604ba --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Core.kt @@ -0,0 +1,91 @@ +package org.appdevforall.codeonthego.indexing.api + +/** + * Any object that can be stored in an index. + * + * The only requirements are a unique key (for deduplication and + * point lookups) and a source identifier (for bulk operations + * when the source changes). + * + * What constitutes a "key" and "source" depends entirely on + * the consumer: + * - For Kotlin symbols: key = FQN, source = JAR path or file path + * - For Android resources: key = resource ID, source = AAR path + * - For Python symbols: key = qualified name, source = .py file path + */ +interface Indexable { + + /** Unique identifier within the index. */ + val key: String + + /** + * Identifies the origin of this entry. + * All entries sharing a [sourceId] can be removed atomically + * via [WritableIndex.removeBySource]. + */ + val sourceId: String +} + +/** + * Describes how to index, serialize, and query a specific type. + * + * Acts as the bridge between domain objects and the storage layer. + * A single index instance is parameterized by one descriptor - + * different data types get different index instances. + * + * @param T The domain type being indexed. + */ +interface IndexDescriptor { + + /** + * A unique name for this index type. Used as the table name + * in persistent storage and the namespace in composite indexes. + */ + val name: String + + /** + * The fields that should be queryable. + * Defines the "schema" for this index type. + * + * The persistent layer will create SQL columns and indexes + * for each declared field. + */ + val fields: List + + /** + * Extract the queryable field values from an entry. + * + * The returned map's keys must be a subset of [fields]'s names. + * Null values mean the field is not applicable for this entry + * (e.g. receiverType is null for a non-extension function). + */ + fun fieldValues(entry: T): Map + + /** + * Serialize an entry to bytes for persistent storage. + * + * Use whatever format is appropriate - protobuf, JSON, + * custom binary. Called once on insert; the bytes are + * stored opaquely. + */ + fun serialize(entry: T): ByteArray + + /** + * Deserialize bytes back into an entry. + * Must be the inverse of [serialize]. + */ + fun deserialize(bytes: ByteArray): T +} + +/** + * Declares a queryable field on an [IndexDescriptor]. + * + * @param name The field name (used in queries and as the column name). + * @param prefixSearchable Whether this field supports prefix queries + * (e.g. name prefix for completions). Affects how + * the persistent layer creates SQL indexes. + */ +data class IndexField( + val name: String, + val prefixSearchable: Boolean = false, +) 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 new file mode 100644 index 0000000000..39f9846494 --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Index.kt @@ -0,0 +1,105 @@ +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. + * + * @param T The indexed type. + */ +interface ReadableIndex { + + /** + * Query the index. Returns a lazy [Flow] 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 + + /** + * Point lookup by key. Returns null if not found. + */ + suspend fun get(key: String): T? + + /** + * Fast existence check for a source. + */ + suspend fun containsSource(sourceId: String): Boolean + + /** + * Returns distinct values for a given field across all entries. + * + * Useful for enumerating packages, kinds, etc. without + * deserializing full entries. + * + * @param fieldName Must be one of the fields declared in the + * [IndexDescriptor]. + */ + fun distinctValues(fieldName: String): Flow +} + +/** + * 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]. + * + * Entries are consumed lazily from the flow 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) + + /** + * Convenience: insert a single entry. + */ + suspend fun insert(entry: T) + + /** + * Remove all entries from the given source. + */ + suspend fun removeBySource(sourceId: String) + + /** + * Remove all entries. + */ + suspend fun clear() +} + +/** + * A complete index with read, write, and lifecycle management. + * + * @param T The indexed type. + */ +interface Index : ReadableIndex, WritableIndex, Closeable { + + /** Human-readable name for logging. */ + val name: String + + /** The descriptor governing serialization and field extraction. */ + val descriptor: IndexDescriptor + + override fun close() {} +} diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Query.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Query.kt new file mode 100644 index 0000000000..8b3e1a6a12 --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Query.kt @@ -0,0 +1,78 @@ +package org.appdevforall.codeonthego.indexing.api + +/** + * A query against an index. + * + * All predicates are ANDed together. The query is intentionally + * field-based (not type-specific) so the same query engine works + * for Kotlin symbols, Android resources, Python declarations, etc. + */ +data class IndexQuery( + /** Exact match predicates: field name → expected value. */ + val exactMatch: Map = emptyMap(), + + /** Prefix match predicates: field name → prefix (case-insensitive). */ + val prefixMatch: Map = emptyMap(), + + /** + * Presence predicates: field name → whether the field must be + * non-null (true) or null (false). + */ + val presence: Map = emptyMap(), + + /** Filter by source ID. */ + val sourceId: String? = null, + + /** Filter by key (exact). */ + val key: String? = null, + + /** Maximum number of results. 0 = unlimited (use with care). */ + val limit: Int = 200, +) { + companion object { + /** Match everything up to [limit]. */ + val ALL = IndexQuery() + + /** Exact key lookup. */ + fun byKey(key: String) = IndexQuery(key = key, limit = 1) + + /** All entries from a specific source. */ + fun bySource(sourceId: String) = IndexQuery(sourceId = sourceId, limit = 0) + } +} + +/** + * DSL builder for [IndexQuery]. + */ +class IndexQueryBuilder { + private val exact = mutableMapOf() + private val prefix = mutableMapOf() + private val pres = mutableMapOf() + var sourceId: String? = null + var key: String? = null + var limit: Int = 200 + + /** Exact match on a field. */ + fun eq(field: String, value: String) { exact[field] = value } + + /** Prefix match on a field (case-insensitive). */ + fun prefix(field: String, value: String) { prefix[field] = value } + + /** Field must be non-null. */ + fun exists(field: String) { pres[field] = true } + + /** Field must be null. */ + fun notExists(field: String) { pres[field] = false } + + fun build() = IndexQuery( + exactMatch = exact.toMap(), + prefixMatch = prefix.toMap(), + presence = pres.toMap(), + sourceId = sourceId, + key = key, + limit = limit, + ) +} + +inline fun indexQuery(block: IndexQueryBuilder.() -> Unit): IndexQuery = + IndexQueryBuilder().apply(block).build() diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexRegistry.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexRegistry.kt new file mode 100644 index 0000000000..e2639c5cae --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexRegistry.kt @@ -0,0 +1,119 @@ +package org.appdevforall.codeonthego.indexing.service + +import java.io.Closeable +import java.util.concurrent.ConcurrentHashMap + +/** + * A typed key for retrieving an index from the [IndexRegistry]. + * + * @param T The index type. Not restricted to [org.appdevforall.codeonthego.indexing.api.Index], can be a + * domain-specific facade. + */ +data class IndexKey( + val name: String, +) + +/** + * Central registry where [IndexingService]s publish their indexes + * and consumers (LSPs, etc.) retrieve them. + */ +class IndexRegistry : Closeable { + + private val indexes = ConcurrentHashMap() + private val listeners = ConcurrentHashMap Unit>>() + + /** + * Register an index. Replaces any previously registered index + * with the same key. + * + * If there are listeners waiting for this key, they are notified + * immediately. + */ + fun register(key: IndexKey, index: T) { + val old = indexes.put(key.name, index) + + // Close the old index if it's Closeable + if (old is Closeable && old !== index) { + old.close() + } + + // Notify listeners + listeners[key.name]?.forEach { listener -> + @Suppress("UNCHECKED_CAST") + (listener as (T) -> Unit).invoke(index) + } + } + + /** + * Retrieve an index by key. Returns null if not yet registered. + */ + @Suppress("UNCHECKED_CAST") + fun get(key: IndexKey): T? = + indexes[key.name] as? T + + /** + * Retrieve an index, throwing if not available. + */ + fun require(key: IndexKey): T = + get(key) ?: throw IllegalStateException( + "Index '${key.name}' is not registered. " + + "Has the corresponding IndexingService been initialized?" + ) + + /** + * Execute a block if the index is available. + */ + inline fun ifAvailable( + key: IndexKey, + block: (T) -> R, + ): R? { + val index = get(key) ?: return null + return block(index) + } + + /** + * Register a listener that will be called when an index + * is registered (or re-registered) with the given key. + * + * If the index is already registered, the listener is + * called immediately. + */ + fun onAvailable(key: IndexKey, listener: (T) -> Unit) { + @Suppress("UNCHECKED_CAST") + listeners.getOrPut(key.name) { mutableListOf() } + .add(listener as (Any) -> Unit) + + // If already registered, notify immediately + get(key)?.let { listener(it) } + } + + /** + * Unregister an index. The caller is responsible for closing it. + */ + fun unregister(key: IndexKey): T? { + @Suppress("UNCHECKED_CAST") + return indexes.remove(key.name) as? T + } + + /** + * Returns true if an index is registered for this key. + */ + fun isRegistered(key: IndexKey): Boolean = + indexes.containsKey(key.name) + + /** + * Returns all registered keys. + */ + fun registeredKeys(): Set = indexes.keys.toSet() + + /** + * Close and remove all registered indexes. + */ + override fun close() { + indexes.values.forEach { index -> + if (index is Closeable) index.close() + } + indexes.clear() + listeners.clear() + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..cbf074cce2 --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingService.kt @@ -0,0 +1,41 @@ +package org.appdevforall.codeonthego.indexing.service +import java.io.Closeable + +/** + * A service that knows how to build and maintain an index for a + * specific domain. + * + * Implementations should be stateless with respect to the project + * model because they receive it as a parameter, not as a constructor + * argument. This allows the same service instance to handle + * re-syncs without recreation. + */ +interface IndexingService : Closeable { + + /** + * Unique identifier for this service. + * Used for logging and debugging. + */ + val id: String + + /** + * The keys of the indexes this service registers. + * Used by the manager to verify all expected indexes + * are available after initialization. + */ + val providedKeys: List> + + /** + * Called once after the service is registered. + * + * Create your index instances here and register them + * with the [registry]. + */ + suspend fun initialize(registry: IndexRegistry) + + /** + * Called when the project is closed or the IDE shuts down. + * Release all resources. + */ + override fun close() {} +} \ No newline at end of file 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 new file mode 100644 index 0000000000..6575c5df63 --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt @@ -0,0 +1,158 @@ +package org.appdevforall.codeonthego.indexing.service + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory +import java.io.Closeable +import java.util.concurrent.ConcurrentHashMap + +/** + * Manages the lifecycle of [IndexingService]s and the [IndexRegistry]. + */ +class IndexingServiceManager( + private val scope: CoroutineScope = CoroutineScope( + SupervisorJob() + Dispatchers.Default + ), +) : Closeable { + + companion object { + private val log = LoggerFactory.getLogger(IndexingServiceManager::class.java) + } + + /** + * The central registry. All services register their indexes here. + * Consumers (LSPs, etc.) retrieve indexes from here. + */ + val registry = IndexRegistry() + + private val services = ConcurrentHashMap() + private var initialized = false + + /** + * Register an [IndexingService]. + * + * Must be called before [onProjectSynced]. Services are initialized + * in registration order. + * + * @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 + } + + log.info("Registered indexing service: {}", service.id) + } + + /** + * Called after project sync (e.g. Gradle sync) completes. + * + * On the first call, initializes all registered services + * (creates indexes, registers them). On subsequent calls, + * notifies services of the updated project model. + * + * Services process the event concurrently. Failures in one + * service don't affect others (SupervisorJob). + */ + fun onProjectSynced() { + scope.launch { + if (!initialized) { + initializeServices() + initialized = true + } + } + } + + /** + * Called after a build completes. + */ + fun onBuildCompleted() { + if (!initialized) { + log.warn("onBuildCompleted called before initialization, ignoring") + return + } + } + + /** + * Called when source files change. + */ + fun onSourceChanged() { + if (!initialized) return + } + + /** + * Returns the registered service with the given ID, or null. + */ + fun getService(id: String): IndexingService? = + services[id] + + /** + * Returns all registered services. + */ + fun allServices(): List = + services.values.toList() + + /** + * Shut down all services and clear the registry. + */ + override fun close() { + log.info("Shutting down indexing services") + + // Cancel in-flight work + scope.coroutineContext.cancelChildren() + + // Close services in reverse registration order + services.values.reversed().forEach { service -> + try { + service.close() + log.debug("Closed service: {}", service.id) + } catch (e: Exception) { + log.error("Failed to close service: {}", service.id, e) + } + } + + services.clear() + registry.close() + initialized = false + + log.info("Indexing services shut down") + } + + private suspend fun initializeServices() { + log.info("Initializing {} indexing services", services.size) + + val allServices = allServices() + for (service in allServices) { + try { + service.initialize(registry) + log.info("Initialized service: {} (provides: {})", + service.id, + service.providedKeys.joinToString { it.name }, + ) + } catch (e: Exception) { + log.error("Failed to initialize service: {}", service.id, e) + } + } + + // Verify all promised keys are registered + for (service in allServices) { + for (key in service.providedKeys) { + if (!registry.isRegistered(key)) { + log.warn( + "Service '{}' promised index '{}' but did not register it", + service.id, key.name, + ) + } + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..1ab75e4074 --- /dev/null +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt @@ -0,0 +1,214 @@ +package org.appdevforall.codeonthego.indexing.util + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +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 +import org.appdevforall.codeonthego.indexing.api.Index +import org.appdevforall.codeonthego.indexing.api.Indexable +import org.slf4j.LoggerFactory +import java.io.Closeable +import java.util.concurrent.ConcurrentHashMap + +/** + * Callback for tracking indexing progress. + * Implementations must be thread-safe. + */ +fun interface IndexingProgressListener { + + /** + * Called with progress updates during indexing. + * + * @param sourceId The source being indexed. + * @param event What happened. + */ + fun onProgress(sourceId: String, event: IndexingEvent) +} + +sealed class IndexingEvent { + data object Started : IndexingEvent() + data class Progress(val processed: Int) : IndexingEvent() + data class Completed(val totalIndexed: Int) : IndexingEvent() + data class Failed(val error: Throwable) : IndexingEvent() + data object Skipped : IndexingEvent() +} + +/** + * Runs indexing operations in the background. + */ +class BackgroundIndexer( + private val index: Index, + 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 { + private val log = LoggerFactory.getLogger(BackgroundIndexer::class.java) + } + + var progressListener: IndexingProgressListener? = null + + private val activeJobs = ConcurrentHashMap() + + /** + * Index a single source. The [provider] returns a [Flow] that + * lazily produces entries so that it is NOT collected eagerly. + * + * 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. + */ + fun indexSource( + sourceId: String, + skipIfExists: Boolean = true, + provider: (sourceId: String) -> Flow, + ): Job { + // Cancel any in-flight job for this source + activeJobs[sourceId]?.cancel() + + val job = scope.launch { + try { + if (skipIfExists && index.containsSource(sourceId)) { + log.debug("Skipping already-indexed: {}", sourceId) + progressListener?.onProgress(sourceId, IndexingEvent.Skipped) + return@launch + } + + log.info("Indexing: {}", sourceId) + + // Remove stale entries first + index.removeBySource(sourceId) + + 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) + ) + } + + // 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) + ) + } + } + } + + index.insert(counted) + + } 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) + ) + } finally { + activeJobs.remove(sourceId) + } + } + + activeJobs[sourceId] = job + return job + } + + /** + * Index multiple sources in parallel. + * + * 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. + */ + fun indexSources( + sources: Collection, + skipIfExists: Boolean = true, + mapper: (S) -> Pair>, + ): List { + return sources.map { source -> + val (sourceId, flow) = mapper(source) + indexSource(sourceId, skipIfExists) { flow } + }.filterNotNull() + } + + /** + * Cancel all in-flight indexing and wait for completion. + */ + suspend fun cancelAll() { + activeJobs.values.toList().forEach { it.cancelAndJoin() } + } + + /** + * Wait for all in-flight indexing to complete. + */ + suspend fun awaitAll() { + activeJobs.values.toList().joinAll() + } + + /** + * Returns the number of currently active indexing jobs. + */ + val activeJobCount: Int get() = activeJobs.size + + override fun close() { + activeJobs.values.forEach { it.cancel() } + activeJobs.clear() + } +} diff --git a/lsp/java/build.gradle.kts b/lsp/java/build.gradle.kts index 7d43f1aefd..73322f24be 100644 --- a/lsp/java/build.gradle.kts +++ b/lsp/java/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(projects.editorApi) implementation(projects.resources) implementation(projects.lsp.api) + implementation(projects.lsp.jvmSymbolIndex) implementation(projects.subprojects.libjdwp) implementation(projects.subprojects.javacServices) implementation(projects.idetooltips) 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 c5f096f702..f5c1fb627c 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 @@ -17,6 +17,7 @@ package com.itsaky.androidide.lsp.java import androidx.annotation.RestrictTo +import com.itsaky.androidide.app.BaseApplication import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent @@ -43,7 +44,7 @@ import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider import com.itsaky.androidide.lsp.java.providers.JavaSelectionProvider import com.itsaky.androidide.lsp.java.providers.ReferenceProvider import com.itsaky.androidide.lsp.java.providers.SignatureProvider -import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository.init +import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer import com.itsaky.androidide.lsp.java.utils.CancelChecker.Companion.isCancelled import com.itsaky.androidide.lsp.models.CodeFormatResult @@ -73,6 +74,7 @@ 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.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -117,7 +119,12 @@ class JavaLanguageServer : ILanguageServer { EventBus.getDefault().register(this) } - init() + val projectManager = ProjectManagerImpl.getInstance() + projectManager.indexingServiceManager.register( + service = JvmIndexingService(context = BaseApplication.baseInstance) + ) + + JavaSnippetRepository.init() } override fun shutdown() { @@ -150,6 +157,11 @@ class JavaLanguageServer : ILanguageServer { override fun setupWithProject(workspace: Workspace) { LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu) + (ProjectManagerImpl.getInstance() + .indexingServiceManager + .getService(JvmIndexingService.ID) as? JvmIndexingService?) + ?.refresh() + // Once we have project initialized // Destory the NO_MODULE_COMPILER instance JavaCompilerService.NO_MODULE_COMPILER.destroy() @@ -247,7 +259,8 @@ class JavaLanguageServer : ILanguageServer { } } - override fun formatCode(params: FormatCodeParams?): CodeFormatResult = CodeFormatProvider(settings).format(params) + override fun formatCode(params: FormatCodeParams?): CodeFormatResult = + CodeFormatProvider(settings).format(params) override fun handleFailure(failure: LSPFailure?): Boolean { return when (failure!!.type) { diff --git a/lsp/jvm-symbol-index/build.gradle.kts b/lsp/jvm-symbol-index/build.gradle.kts new file mode 100644 index 0000000000..959f2264be --- /dev/null +++ b/lsp/jvm-symbol-index/build.gradle.kts @@ -0,0 +1,24 @@ +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + alias(libs.plugins.android.library) + id("kotlin-android") +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.lsp.java.indexing" +} + +dependencies { + api(libs.google.protobuf.java) + api(libs.google.protobuf.kotlin) + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.metadata) + + api(projects.common) + api(projects.logger) + api(projects.lsp.indexing) + api(projects.lsp.jvmSymbolModels) + api(projects.subprojects.kotlinAnalysisApi) + api(projects.subprojects.projects) +} 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 new file mode 100644 index 0000000000..f0bea84939 --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt @@ -0,0 +1,78 @@ +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.org.objectweb.asm.AnnotationVisitor +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.ClassVisitor +import org.jetbrains.org.objectweb.asm.Opcodes +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import java.nio.file.Path +import java.util.jar.JarFile +import kotlin.io.path.pathString + +/** + * Scans a JAR and routes each class to the appropriate scanner: + * [KotlinMetadataScanner] for Kotlin classes, [JarSymbolScanner] for Java. + */ +object CombinedJarScanner { + + private val log = LoggerFactory.getLogger(CombinedJarScanner::class.java) + + fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { + val jar = try { + JarFile(jarPath.toFile()) + } catch (e: Exception) { + log.warn("Failed to open JAR: {}", jarPath, e) + return@flow + } + + jar.use { + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (!entry.name.endsWith(".class")) continue + if (entry.name == "module-info.class" || entry.name == "package-info.class") continue + + try { + val bytes = jar.getInputStream(entry).use { input -> + val buf = ByteArrayOutputStream(entry.size.toInt().coerceAtLeast(1024)) + input.copyTo(buf) + buf.toByteArray() + } + + val symbols = if (hasKotlinMetadata(bytes)) { + KotlinMetadataScanner.parseKotlinClass(bytes.inputStream(), sourceId) + } else { + JarSymbolScanner.parseClassFile(bytes.inputStream(), sourceId) + } + + symbols?.forEach { emit(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 + try { + ClassReader(classBytes).accept(object : ClassVisitor(Opcodes.ASM9) { + override fun visitAnnotation( + descriptor: String?, + visible: Boolean + ): AnnotationVisitor? { + if (descriptor == "Lkotlin/Metadata;") found = true + return null + } + }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) + } catch (_: Exception) { + } + return found + } +} 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 new file mode 100644 index 0000000000..89fd0ab492 --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt @@ -0,0 +1,305 @@ +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.org.objectweb.asm.AnnotationVisitor +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.ClassVisitor +import org.jetbrains.org.objectweb.asm.FieldVisitor +import org.jetbrains.org.objectweb.asm.MethodVisitor +import org.jetbrains.org.objectweb.asm.Opcodes +import org.jetbrains.org.objectweb.asm.Type +import org.slf4j.LoggerFactory +import java.io.InputStream +import java.nio.file.Path +import java.util.jar.JarFile +import kotlin.io.path.pathString + +/** + * Scans JAR files using ASM and produces [JvmSymbol]s lazily. + * + * For Java class files, this gives complete information. + * For Kotlin class files, use [KotlinMetadataScanner] or + * [CombinedJarScanner] instead — ASM cannot see Kotlin-specific + * semantics like extensions, suspend, or nullable types. + */ +object JarSymbolScanner { + + private val log = LoggerFactory.getLogger(JarSymbolScanner::class.java) + + fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { + val jar = try { + JarFile(jarPath.toFile()) + } catch (e: Exception) { + log.warn("Failed to open JAR: {}", jarPath, e) + return@flow + } + + jar.use { + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (!entry.name.endsWith(".class")) continue + if (entry.name == "module-info.class") continue + if (entry.name == "package-info.class") continue + + try { + jar.getInputStream(entry).use { input -> + for (symbol in parseClassFile(input, sourceId)) { + emit(symbol) + } + } + } catch (e: Exception) { + log.debug("Failed to parse {}: {}", entry.name, e.message) + } + } + } + } + .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.replace('/', '.')) + } + interfaces?.forEach { add(it.replace('/', '.')) } + } + + val containingClass = if (isInnerClass) { + classFqName.split('.').dropLast(1).joinToString(".") + } else "" + + symbols.add( + JvmSymbol( + key = classFqName, + sourceId = sourceId, + fqName = classFqName, + shortName = shortClassName.split('.').last(), + packageName = packageName, + kind = kind, + language = language, + visibility = visibilityFromAccess(classAccess), + isDeprecated = classDeprecated, + data = JvmClassInfo( + containingClassFqName = containingClass, + supertypeFqNames = 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 + typeFqName = typeToFqName(type), + typeDisplay = typeToDisplay(type), + ) + } + + val fqName = "$classFqName.$methodName" + val key = "$fqName(${parameters.joinToString(",") { it.typeFqName }})" + + val signatureDisplay = buildString { + append("(") + append(parameters.joinToString(", ") { it.typeDisplay }) + append(")") + if (!isConstructor) { + append(": ") + append(typeToDisplay(returnType)) + } + } + + symbols.add( + JvmSymbol( + key = key, + sourceId = sourceId, + fqName = fqName, + shortName = methodName, + packageName = packageName, + kind = kind, + language = language, + visibility = visibilityFromAccess(access), + isDeprecated = classDeprecated, + data = JvmFunctionInfo( + containingClassFqName = classFqName, + returnTypeFqName = typeToFqName(returnType), + returnTypeDisplay = typeToDisplay(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 fqName = "$classFqName.$name" + + symbols.add( + JvmSymbol( + key = fqName, + sourceId = sourceId, + fqName = fqName, + shortName = name, + packageName = packageName, + kind = kind, + language = language, + visibility = visibilityFromAccess(access), + isDeprecated = classDeprecated, + data = JvmFieldInfo( + containingClassFqName = classFqName, + typeFqName = typeToFqName(fieldType), + typeDisplay = typeToDisplay(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 typeToFqName(type: Type): String = when (type.sort) { + Type.VOID -> "void" + Type.BOOLEAN -> "boolean" + Type.BYTE -> "byte" + Type.CHAR -> "char" + Type.SHORT -> "short" + Type.INT -> "int" + Type.LONG -> "long" + Type.FLOAT -> "float" + Type.DOUBLE -> "double" + Type.ARRAY -> typeToFqName(type.elementType) + "[]".repeat(type.dimensions) + Type.OBJECT -> type.className + else -> type.className + } + + private fun typeToDisplay(type: Type): String = when (type.sort) { + Type.VOID -> "void" + Type.ARRAY -> typeToDisplay(type.elementType) + "[]".repeat(type.dimensions) + Type.OBJECT -> type.className.substringAfterLast('.') + else -> typeToFqName(type) + } + } +} 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/JvmIndexingService.kt new file mode 100644 index 0000000000..a7cb834f81 --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmIndexingService.kt @@ -0,0 +1,149 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import android.content.Context +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.models.bootClassPaths +import com.itsaky.androidide.tasks.cancelIfActive +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +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.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.slf4j.LoggerFactory +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.pathString + +/** + * Well-known key for the JVM symbol index. + * + * Both the Kotlin and Java LSPs use this key to retrieve the + * shared index from the [IndexRegistry]. + */ +val JVM_SYMBOL_INDEX = IndexKey("jvm-symbols") + +/** + * [IndexingService] that scans classpath JARs/AARs and builds + * a [JvmSymbolIndex]. + * + * Thread safety: all methods are called from the + * [IndexingServiceManager][org.appdevforall.codeonthego.indexing.service.IndexingServiceManager]'s + * coroutine scope. The [JvmSymbolIndex] handles its own internal thread safety. + */ +class JvmIndexingService( + private val context: Context, +) : IndexingService { + + companion object { + const val ID = "jvm-indexing-service" + private val log = LoggerFactory.getLogger(JvmIndexingService::class.java) + } + + override val id = ID + + override val providedKeys = listOf(JVM_SYMBOL_INDEX) + + private var index: JvmSymbolIndex? = null + private var indexingMutex = Mutex() + private val coroutineScope = CoroutineScope(Dispatchers.Default) + + override suspend fun initialize(registry: IndexRegistry) { + val jvmIndex = JvmSymbolIndex.create(context) + this.index = jvmIndex + registry.register(JVM_SYMBOL_INDEX, jvmIndex) + log.info("JVM symbol index initialized") + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("UNUSED") + fun onProjectSynced() { + refresh() + } + + fun refresh() { + coroutineScope.launch { + indexingMutex.withLock { + reindexLibraries() + } + } + } + + private suspend fun reindexLibraries() { + val index = this.index ?: run { + log.warn("Not indexing libraries. Index not initialized.") + return + } + + val workspace = ProjectManagerImpl.getInstance().workspace ?: run { + log.warn("Not indexing libraries. Workspace model not available.") + return + } + + val currentJars = + workspace.subProjects + .asSequence() + .filterIsInstance() + .filter { it.path != workspace.rootProject.path } + .flatMap { project -> + buildList { + if (project is AndroidModule) { + addAll(project.bootClassPaths) + } + + addAll(project.getCompileClasspaths(excludeSourceGeneratedClassPath = true)) + } + } + .filter { jar -> jar.exists() && isIndexableJar(jar.toPath()) } + .map { jar -> jar.absolutePath } + .toSet() + + log.info("{} JARs on classpath", currentJars.size) + + // Step 1: Set the active set - this is instant. + // JARs not in the set become invisible to queries. + // JARs in the set that are already cached become + // visible immediately. + index.setActiveLibraries(currentJars) + + // Step 2: Index any JARs not yet in the cache. + // Already-cached JARs are skipped (cheap existence check). + // Newly cached JARs are automatically visible because + // they're already in the active set. + var newCount = 0 + for (jarPath in currentJars) { + if (!index.isLibraryCached(jarPath)) { + newCount++ + index.indexLibrary(jarPath) { sourceId -> + CombinedJarScanner.scan(Paths.get(jarPath), sourceId) + } + } + } + + if (newCount > 0) { + log.info("{} new JARs submitted for background indexing", newCount) + } else { + log.info("All JARs already cached, nothing to index") + } + } + + override fun close() { + coroutineScope.cancelIfActive("indexing service closed") + index?.close() + index = null + } + + private fun isIndexableJar(path: Path): Boolean { + val ext = path.extension.lowercase() + return ext == "jar" || ext == "aar" + } +} \ 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 new file mode 100644 index 0000000000..fdbd2c20bd --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbol.kt @@ -0,0 +1,204 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import org.appdevforall.codeonthego.indexing.api.Indexable + +enum class JvmSymbolKind { + CLASS, INTERFACE, ENUM, ENUM_ENTRY, ANNOTATION_CLASS, + OBJECT, COMPANION_OBJECT, DATA_CLASS, VALUE_CLASS, + SEALED_CLASS, SEALED_INTERFACE, + FUNCTION, EXTENSION_FUNCTION, CONSTRUCTOR, + PROPERTY, EXTENSION_PROPERTY, FIELD, + TYPE_ALIAS; + + val isCallable: Boolean + get() = this in CALLABLE_KINDS + + val isClassifier: Boolean + get() = this in CLASSIFIER_KINDS + + val isExtension: Boolean + get() = this == EXTENSION_FUNCTION || this == EXTENSION_PROPERTY + + companion object { + val CALLABLE_KINDS = setOf( + FUNCTION, EXTENSION_FUNCTION, CONSTRUCTOR, + PROPERTY, EXTENSION_PROPERTY, FIELD, + ) + val CLASSIFIER_KINDS = setOf( + CLASS, INTERFACE, ENUM, ANNOTATION_CLASS, + OBJECT, COMPANION_OBJECT, DATA_CLASS, + VALUE_CLASS, SEALED_CLASS, SEALED_INTERFACE, + TYPE_ALIAS, + ) + } +} + +enum class JvmSourceLanguage { JAVA, KOTLIN } + +enum class JvmVisibility { + PUBLIC, PROTECTED, INTERNAL, PRIVATE, PACKAGE_PRIVATE; + + val isAccessibleOutsideClass: Boolean + get() = this == PUBLIC || this == PROTECTED || this == INTERNAL +} + +/** + * A symbol from a JVM class file (JAR/AAR). + * + * Common identity fields live here. Type-specific details live in + * [data], which is one of: + * - [JvmClassInfo] for classes, interfaces, enums, objects, etc. + * - [JvmFunctionInfo] for functions, extension functions, constructors + * - [JvmFieldInfo] for Java fields and Kotlin properties + * - [JvmEnumEntryInfo] for enum constants + * - [JvmTypeAliasInfo] for Kotlin type aliases + */ +data class JvmSymbol( + override val key: String, + override val sourceId: String, + + val fqName: String, + val shortName: String, + val packageName: String, + val kind: JvmSymbolKind, + val language: JvmSourceLanguage, + val visibility: JvmVisibility = JvmVisibility.PUBLIC, + val isDeprecated: Boolean = false, + + val data: JvmSymbolInfo, +) : Indexable { + + val isTopLevel: Boolean + get() = data.containingClassFqName.isEmpty() + + val isExtension: Boolean + get() = kind.isExtension + + val receiverTypeFqName: String? + get() = when (val d = data) { + is JvmFunctionInfo -> d.kotlin?.receiverTypeFqName?.takeIf { it.isNotEmpty() } + is JvmFieldInfo -> d.kotlin?.receiverTypeFqName?.takeIf { it.isNotEmpty() } + else -> null + } + + val containingClassFqName: String + get() = data.containingClassFqName + + val returnTypeDisplay: String + get() = when (val d = data) { + is JvmFunctionInfo -> d.returnTypeDisplay + is JvmFieldInfo -> d.typeDisplay + else -> "" + } + + val signatureDisplay: String + get() = when (val d = data) { + is JvmFunctionInfo -> d.signatureDisplay + else -> "" + } +} + +/** + * Base for all type-specific symbol data. + * Every variant provides [containingClassFqName] (empty for top-level). + */ +sealed interface JvmSymbolInfo { + val containingClassFqName: String +} + +data class JvmClassInfo( + override val containingClassFqName: String = "", + val supertypeFqNames: List = emptyList(), + val typeParameters: List = emptyList(), + val isAbstract: Boolean = false, + val isFinal: Boolean = false, + val isInner: Boolean = false, + val isStatic: Boolean = false, + val kotlin: KotlinClassInfo? = null, +) : JvmSymbolInfo + +data class KotlinClassInfo( + val isData: Boolean = false, + val isValue: Boolean = false, + val isSealed: Boolean = false, + val isFunInterface: Boolean = false, + val isExpect: Boolean = false, + val isActual: Boolean = false, + val isExternal: Boolean = false, + val sealedSubclasses: List = emptyList(), + val companionObjectName: String = "", +) + +data class JvmFunctionInfo( + override val containingClassFqName: String = "", + val returnTypeFqName: String = "", + val returnTypeDisplay: String = "", + val parameterCount: Int = 0, + val parameters: List = emptyList(), + val signatureDisplay: String = "", + val typeParameters: List = emptyList(), + val isStatic: Boolean = false, + val isAbstract: Boolean = false, + val isFinal: Boolean = false, + val kotlin: KotlinFunctionInfo? = null, +) : JvmSymbolInfo + +data class JvmParameterInfo( + val name: String, + val typeFqName: String, + val typeDisplay: String, + val hasDefaultValue: Boolean = false, + val isCrossinline: Boolean = false, + val isNoinline: Boolean = false, + val isVararg: Boolean = false, +) + +data class KotlinFunctionInfo( + val receiverTypeFqName: String = "", + val receiverTypeDisplay: String = "", + val isSuspend: Boolean = false, + val isInline: Boolean = false, + val isInfix: Boolean = false, + val isOperator: Boolean = false, + val isTailrec: Boolean = false, + val isExternal: Boolean = false, + val isExpect: Boolean = false, + val isActual: Boolean = false, + val isReturnTypeNullable: Boolean = false, +) + +data class JvmFieldInfo( + override val containingClassFqName: String = "", + val typeFqName: String = "", + val typeDisplay: String = "", + val isStatic: Boolean = false, + val isFinal: Boolean = false, + val constantValue: String = "", + val kotlin: KotlinPropertyInfo? = null, +) : JvmSymbolInfo + +data class KotlinPropertyInfo( + val receiverTypeFqName: String = "", + val receiverTypeDisplay: String = "", + val isConst: Boolean = false, + val isLateinit: Boolean = false, + val hasGetter: Boolean = false, + val hasSetter: Boolean = false, + val isDelegated: Boolean = false, + val isExpect: Boolean = false, + val isActual: Boolean = false, + val isExternal: Boolean = false, + val isTypeNullable: Boolean = false, +) + +data class JvmEnumEntryInfo( + override val containingClassFqName: String = "", + val ordinal: Int = 0, +) : JvmSymbolInfo + +data class JvmTypeAliasInfo( + override val containingClassFqName: String = "", + val expandedTypeFqName: String = "", + val expandedTypeDisplay: String = "", + val typeParameters: List = emptyList(), +) : JvmSymbolInfo diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt new file mode 100644 index 0000000000..949240eafd --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt @@ -0,0 +1,409 @@ +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 org.appdevforall.codeonthego.indexing.jvm.proto.JvmSymbolProtos.JvmSymbolData + +/** + * [IndexDescriptor] for [JvmSymbol]. + * + * Queryable fields: + * - `name` : prefix-searchable, for completion + * - `package` : exact, for package-scoped queries + * - `kind` : exact, for filtering by CLASS/FUNCTION/etc. + * - `receiverType` : exact, for extension function matching + * - `containingClass`: exact, for member lookup + * - `language` : exact, for Java-only or Kotlin-only queries + * + * Blob serialization uses Protobuf with oneof for type-specific data. + */ +object JvmSymbolDescriptor : IndexDescriptor { + + override val name: String = "jvm_symbols" + + override val fields: List = listOf( + IndexField(name = "name", prefixSearchable = true), + IndexField(name = "package"), + IndexField(name = "kind"), + IndexField(name = "receiverType"), + IndexField(name = "containingClass"), + IndexField(name = "language"), + ) + + override fun fieldValues(entry: JvmSymbol): Map = mapOf( + "name" to entry.shortName, + "package" to entry.packageName, + "kind" to entry.kind.name, + "receiverType" to entry.receiverTypeFqName, + "containingClass" to entry.containingClassFqName.ifEmpty { null }, + "language" to entry.language.name, + ) + + override fun serialize(entry: JvmSymbol): ByteArray = + toProto(entry).toByteArray() + + override fun deserialize(bytes: ByteArray): JvmSymbol = + fromProto(JvmSymbolData.parseFrom(bytes)) + + private fun toProto(s: JvmSymbol): JvmSymbolData { + val builder = JvmSymbolData.newBuilder() + .setFqName(s.fqName) + .setShortName(s.shortName) + .setPackageName(s.packageName) + .setSourceId(s.sourceId) + .setKind(kindToProto(s.kind)) + .setLanguage(languageToProto(s.language)) + .setVisibility(visibilityToProto(s.visibility)) + .setIsDeprecated(s.isDeprecated) + + when (val data = s.data) { + is JvmClassInfo -> builder.setClassData(classInfoToProto(data)) + is JvmFunctionInfo -> builder.setFunctionData(functionInfoToProto(data)) + is JvmFieldInfo -> builder.setFieldData(fieldInfoToProto(data)) + is JvmEnumEntryInfo -> builder.setEnumEntryData(enumEntryToProto(data)) + is JvmTypeAliasInfo -> builder.setTypeAliasData(typeAliasToProto(data)) + } + + return builder.build() + } + + private fun classInfoToProto(d: JvmClassInfo): JvmSymbolProtos.ClassData { + val builder = JvmSymbolProtos.ClassData.newBuilder() + .setContainingClassFqName(d.containingClassFqName) + .addAllSupertypeFqNames(d.supertypeFqNames) + .addAllTypeParameters(d.typeParameters) + .setIsAbstract(d.isAbstract) + .setIsFinal(d.isFinal) + .setIsInner(d.isInner) + .setIsStatic(d.isStatic) + + d.kotlin?.let { kd -> + builder.setKotlin( + JvmSymbolProtos.KotlinClassData.newBuilder() + .setIsData(kd.isData) + .setIsValue(kd.isValue) + .setIsSealed(kd.isSealed) + .setIsFunInterface(kd.isFunInterface) + .setIsExpect(kd.isExpect) + .setIsActual(kd.isActual) + .setIsExternal(kd.isExternal) + .addAllSealedSubclasses(kd.sealedSubclasses) + .setCompanionObjectName(kd.companionObjectName) + ) + } + + return builder.build() + } + + private fun functionInfoToProto(d: JvmFunctionInfo): JvmSymbolProtos.FunctionData { + val builder = JvmSymbolProtos.FunctionData.newBuilder() + .setContainingClassFqName(d.containingClassFqName) + .setReturnTypeFqName(d.returnTypeFqName) + .setReturnTypeDisplay(d.returnTypeDisplay) + .setParameterCount(d.parameterCount) + .addAllParameters(d.parameters.map { paramToProto(it) }) + .setSignatureDisplay(d.signatureDisplay) + .addAllTypeParameters(d.typeParameters) + .setIsStatic(d.isStatic) + .setIsAbstract(d.isAbstract) + .setIsFinal(d.isFinal) + + d.kotlin?.let { kd -> + builder.setKotlin( + JvmSymbolProtos.KotlinFunctionData.newBuilder() + .setReceiverTypeFqName(kd.receiverTypeFqName) + .setReceiverTypeDisplay(kd.receiverTypeDisplay) + .setIsSuspend(kd.isSuspend) + .setIsInline(kd.isInline) + .setIsInfix(kd.isInfix) + .setIsOperator(kd.isOperator) + .setIsTailrec(kd.isTailrec) + .setIsExternal(kd.isExternal) + .setIsExpect(kd.isExpect) + .setIsActual(kd.isActual) + .setIsReturnTypeNullable(kd.isReturnTypeNullable) + ) + } + + return builder.build() + } + + private fun paramToProto(p: JvmParameterInfo): JvmSymbolProtos.ParameterData = + JvmSymbolProtos.ParameterData.newBuilder() + .setName(p.name) + .setTypeFqName(p.typeFqName) + .setTypeDisplay(p.typeDisplay) + .setHasDefaultValue(p.hasDefaultValue) + .setIsCrossinline(p.isCrossinline) + .setIsNoinline(p.isNoinline) + .setIsVararg(p.isVararg) + .build() + + private fun fieldInfoToProto(d: JvmFieldInfo): JvmSymbolProtos.FieldData { + val builder = JvmSymbolProtos.FieldData.newBuilder() + .setContainingClassFqName(d.containingClassFqName) + .setTypeFqName(d.typeFqName) + .setTypeDisplay(d.typeDisplay) + .setIsStatic(d.isStatic) + .setIsFinal(d.isFinal) + .setConstantValue(d.constantValue) + + d.kotlin?.let { kd -> + builder.setKotlin( + JvmSymbolProtos.KotlinPropertyData.newBuilder() + .setReceiverTypeFqName(kd.receiverTypeFqName) + .setReceiverTypeDisplay(kd.receiverTypeDisplay) + .setIsConst(kd.isConst) + .setIsLateinit(kd.isLateinit) + .setHasGetter(kd.hasGetter) + .setHasSetter(kd.hasSetter) + .setIsDelegated(kd.isDelegated) + .setIsExpect(kd.isExpect) + .setIsActual(kd.isActual) + .setIsExternal(kd.isExternal) + .setIsTypeNullable(kd.isTypeNullable) + ) + } + + return builder.build() + } + + private fun enumEntryToProto(d: JvmEnumEntryInfo): JvmSymbolProtos.EnumEntryData = + JvmSymbolProtos.EnumEntryData.newBuilder() + .setContainingEnumFqName(d.containingClassFqName) + .setOrdinal(d.ordinal) + .build() + + private fun typeAliasToProto(d: JvmTypeAliasInfo): JvmSymbolProtos.TypeAliasData = + JvmSymbolProtos.TypeAliasData.newBuilder() + .setExpandedTypeFqName(d.expandedTypeFqName) + .setExpandedTypeDisplay(d.expandedTypeDisplay) + .addAllTypeParameters(d.typeParameters) + .build() + + private fun fromProto(p: JvmSymbolData): JvmSymbol { + val kind = kindFromProto(p.kind) + val data = dataFromProto(p) + + val key = when { + kind.isCallable && kind != JvmSymbolKind.PROPERTY + && kind != JvmSymbolKind.EXTENSION_PROPERTY + && kind != JvmSymbolKind.FIELD -> { + val params = (data as? JvmFunctionInfo) + ?.parameters + ?.joinToString(",") { it.typeFqName } + ?: "" + "${p.fqName}($params)" + } + else -> p.fqName + } + + return JvmSymbol( + key = key, + sourceId = p.sourceId, + fqName = p.fqName, + shortName = p.shortName, + packageName = p.packageName, + kind = kind, + language = languageFromProto(p.language), + visibility = visibilityFromProto(p.visibility), + isDeprecated = p.isDeprecated, + data = data, + ) + } + + private fun dataFromProto(p: JvmSymbolData): JvmSymbolInfo = when (p.dataCase) { + JvmSymbolData.DataCase.CLASS_DATA -> classInfoFromProto(p.classData) + JvmSymbolData.DataCase.FUNCTION_DATA -> functionInfoFromProto(p.functionData) + JvmSymbolData.DataCase.FIELD_DATA -> fieldInfoFromProto(p.fieldData) + JvmSymbolData.DataCase.ENUM_ENTRY_DATA -> enumEntryFromProto(p.enumEntryData) + JvmSymbolData.DataCase.TYPE_ALIAS_DATA -> typeAliasFromProto(p.typeAliasData) + else -> JvmClassInfo() // fallback + } + + private fun classInfoFromProto(p: JvmSymbolProtos.ClassData): JvmClassInfo { + val kotlin = if (p.hasKotlin()) { + val kd = p.kotlin + KotlinClassInfo( + isData = kd.isData, + isValue = kd.isValue, + isSealed = kd.isSealed, + isFunInterface = kd.isFunInterface, + isExpect = kd.isExpect, + isActual = kd.isActual, + isExternal = kd.isExternal, + sealedSubclasses = kd.sealedSubclassesList.toList(), + companionObjectName = kd.companionObjectName, + ) + } else null + + return JvmClassInfo( + containingClassFqName = p.containingClassFqName, + supertypeFqNames = p.supertypeFqNamesList.toList(), + typeParameters = p.typeParametersList.toList(), + isAbstract = p.isAbstract, + isFinal = p.isFinal, + isInner = p.isInner, + isStatic = p.isStatic, + kotlin = kotlin, + ) + } + + private fun functionInfoFromProto(p: JvmSymbolProtos.FunctionData): JvmFunctionInfo { + val kotlin = if (p.hasKotlin()) { + val kd = p.kotlin + KotlinFunctionInfo( + receiverTypeFqName = kd.receiverTypeFqName, + receiverTypeDisplay = kd.receiverTypeDisplay, + isSuspend = kd.isSuspend, + isInline = kd.isInline, + isInfix = kd.isInfix, + isOperator = kd.isOperator, + isTailrec = kd.isTailrec, + isExternal = kd.isExternal, + isExpect = kd.isExpect, + isActual = kd.isActual, + isReturnTypeNullable = kd.isReturnTypeNullable, + ) + } else null + + return JvmFunctionInfo( + containingClassFqName = p.containingClassFqName, + returnTypeFqName = p.returnTypeFqName, + returnTypeDisplay = p.returnTypeDisplay, + parameterCount = p.parameterCount, + parameters = p.parametersList.map { paramFromProto(it) }, + signatureDisplay = p.signatureDisplay, + typeParameters = p.typeParametersList.toList(), + isStatic = p.isStatic, + isAbstract = p.isAbstract, + isFinal = p.isFinal, + kotlin = kotlin, + ) + } + + private fun paramFromProto(p: JvmSymbolProtos.ParameterData): JvmParameterInfo = + JvmParameterInfo( + name = p.name, + typeFqName = p.typeFqName, + typeDisplay = p.typeDisplay, + hasDefaultValue = p.hasDefaultValue, + isCrossinline = p.isCrossinline, + isNoinline = p.isNoinline, + isVararg = p.isVararg, + ) + + private fun fieldInfoFromProto(p: JvmSymbolProtos.FieldData): JvmFieldInfo { + val kotlin = if (p.hasKotlin()) { + val kd = p.kotlin + KotlinPropertyInfo( + receiverTypeFqName = kd.receiverTypeFqName, + receiverTypeDisplay = kd.receiverTypeDisplay, + isConst = kd.isConst, + isLateinit = kd.isLateinit, + hasGetter = kd.hasGetter, + hasSetter = kd.hasSetter, + isDelegated = kd.isDelegated, + isExpect = kd.isExpect, + isActual = kd.isActual, + isExternal = kd.isExternal, + isTypeNullable = kd.isTypeNullable, + ) + } else null + + return JvmFieldInfo( + containingClassFqName = p.containingClassFqName, + typeFqName = p.typeFqName, + typeDisplay = p.typeDisplay, + isStatic = p.isStatic, + isFinal = p.isFinal, + constantValue = p.constantValue, + kotlin = kotlin, + ) + } + + private fun enumEntryFromProto(p: JvmSymbolProtos.EnumEntryData): JvmEnumEntryInfo = + JvmEnumEntryInfo( + containingClassFqName = p.containingEnumFqName, + ordinal = p.ordinal, + ) + + private fun typeAliasFromProto(p: JvmSymbolProtos.TypeAliasData): JvmTypeAliasInfo = + JvmTypeAliasInfo( + expandedTypeFqName = p.expandedTypeFqName, + expandedTypeDisplay = p.expandedTypeDisplay, + typeParameters = p.typeParametersList.toList(), + ) + + private fun kindToProto(k: JvmSymbolKind) = when (k) { + JvmSymbolKind.CLASS -> JvmSymbolProtos.JvmSymbolKind.KIND_CLASS + JvmSymbolKind.INTERFACE -> JvmSymbolProtos.JvmSymbolKind.KIND_INTERFACE + JvmSymbolKind.ENUM -> JvmSymbolProtos.JvmSymbolKind.KIND_ENUM + JvmSymbolKind.ENUM_ENTRY -> JvmSymbolProtos.JvmSymbolKind.KIND_ENUM_ENTRY + JvmSymbolKind.ANNOTATION_CLASS -> JvmSymbolProtos.JvmSymbolKind.KIND_ANNOTATION_CLASS + JvmSymbolKind.OBJECT -> JvmSymbolProtos.JvmSymbolKind.KIND_OBJECT + JvmSymbolKind.COMPANION_OBJECT -> JvmSymbolProtos.JvmSymbolKind.KIND_COMPANION_OBJECT + JvmSymbolKind.DATA_CLASS -> JvmSymbolProtos.JvmSymbolKind.KIND_DATA_CLASS + JvmSymbolKind.VALUE_CLASS -> JvmSymbolProtos.JvmSymbolKind.KIND_VALUE_CLASS + JvmSymbolKind.SEALED_CLASS -> JvmSymbolProtos.JvmSymbolKind.KIND_SEALED_CLASS + JvmSymbolKind.SEALED_INTERFACE -> JvmSymbolProtos.JvmSymbolKind.KIND_SEALED_INTERFACE + JvmSymbolKind.FUNCTION -> JvmSymbolProtos.JvmSymbolKind.KIND_FUNCTION + JvmSymbolKind.EXTENSION_FUNCTION -> JvmSymbolProtos.JvmSymbolKind.KIND_EXTENSION_FUNCTION + JvmSymbolKind.CONSTRUCTOR -> JvmSymbolProtos.JvmSymbolKind.KIND_CONSTRUCTOR + JvmSymbolKind.PROPERTY -> JvmSymbolProtos.JvmSymbolKind.KIND_PROPERTY + JvmSymbolKind.EXTENSION_PROPERTY -> JvmSymbolProtos.JvmSymbolKind.KIND_EXTENSION_PROPERTY + JvmSymbolKind.FIELD -> JvmSymbolProtos.JvmSymbolKind.KIND_FIELD + JvmSymbolKind.TYPE_ALIAS -> JvmSymbolProtos.JvmSymbolKind.KIND_TYPE_ALIAS + } + + private fun kindFromProto(k: JvmSymbolProtos.JvmSymbolKind) = when (k) { + JvmSymbolProtos.JvmSymbolKind.KIND_CLASS -> JvmSymbolKind.CLASS + JvmSymbolProtos.JvmSymbolKind.KIND_INTERFACE -> JvmSymbolKind.INTERFACE + JvmSymbolProtos.JvmSymbolKind.KIND_ENUM -> JvmSymbolKind.ENUM + JvmSymbolProtos.JvmSymbolKind.KIND_ENUM_ENTRY -> JvmSymbolKind.ENUM_ENTRY + JvmSymbolProtos.JvmSymbolKind.KIND_ANNOTATION_CLASS -> JvmSymbolKind.ANNOTATION_CLASS + JvmSymbolProtos.JvmSymbolKind.KIND_OBJECT -> JvmSymbolKind.OBJECT + JvmSymbolProtos.JvmSymbolKind.KIND_COMPANION_OBJECT -> JvmSymbolKind.COMPANION_OBJECT + JvmSymbolProtos.JvmSymbolKind.KIND_DATA_CLASS -> JvmSymbolKind.DATA_CLASS + JvmSymbolProtos.JvmSymbolKind.KIND_VALUE_CLASS -> JvmSymbolKind.VALUE_CLASS + JvmSymbolProtos.JvmSymbolKind.KIND_SEALED_CLASS -> JvmSymbolKind.SEALED_CLASS + JvmSymbolProtos.JvmSymbolKind.KIND_SEALED_INTERFACE -> JvmSymbolKind.SEALED_INTERFACE + JvmSymbolProtos.JvmSymbolKind.KIND_FUNCTION -> JvmSymbolKind.FUNCTION + JvmSymbolProtos.JvmSymbolKind.KIND_EXTENSION_FUNCTION -> JvmSymbolKind.EXTENSION_FUNCTION + JvmSymbolProtos.JvmSymbolKind.KIND_CONSTRUCTOR -> JvmSymbolKind.CONSTRUCTOR + JvmSymbolProtos.JvmSymbolKind.KIND_PROPERTY -> JvmSymbolKind.PROPERTY + JvmSymbolProtos.JvmSymbolKind.KIND_EXTENSION_PROPERTY -> JvmSymbolKind.EXTENSION_PROPERTY + JvmSymbolProtos.JvmSymbolKind.KIND_FIELD -> JvmSymbolKind.FIELD + JvmSymbolProtos.JvmSymbolKind.KIND_TYPE_ALIAS -> JvmSymbolKind.TYPE_ALIAS + else -> JvmSymbolKind.CLASS + } + + private fun languageToProto(l: JvmSourceLanguage) = when (l) { + JvmSourceLanguage.JAVA -> JvmSymbolProtos.JvmSourceLanguage.LANGUAGE_JAVA + JvmSourceLanguage.KOTLIN -> JvmSymbolProtos.JvmSourceLanguage.LANGUAGE_KOTLIN + } + + private fun languageFromProto(l: JvmSymbolProtos.JvmSourceLanguage) = when (l) { + JvmSymbolProtos.JvmSourceLanguage.LANGUAGE_JAVA -> JvmSourceLanguage.JAVA + JvmSymbolProtos.JvmSourceLanguage.LANGUAGE_KOTLIN -> JvmSourceLanguage.KOTLIN + else -> JvmSourceLanguage.JAVA + } + + private fun visibilityToProto(v: JvmVisibility) = when (v) { + JvmVisibility.PUBLIC -> JvmSymbolProtos.JvmVisibility.VISIBILITY_PUBLIC + JvmVisibility.PROTECTED -> JvmSymbolProtos.JvmVisibility.VISIBILITY_PROTECTED + JvmVisibility.INTERNAL -> JvmSymbolProtos.JvmVisibility.VISIBILITY_INTERNAL + JvmVisibility.PRIVATE -> JvmSymbolProtos.JvmVisibility.VISIBILITY_PRIVATE + JvmVisibility.PACKAGE_PRIVATE -> JvmSymbolProtos.JvmVisibility.VISIBILITY_PACKAGE_PRIVATE + } + + private fun visibilityFromProto(v: JvmSymbolProtos.JvmVisibility) = when (v) { + JvmSymbolProtos.JvmVisibility.VISIBILITY_PUBLIC -> JvmVisibility.PUBLIC + JvmSymbolProtos.JvmVisibility.VISIBILITY_PROTECTED -> JvmVisibility.PROTECTED + JvmSymbolProtos.JvmVisibility.VISIBILITY_INTERNAL -> JvmVisibility.INTERNAL + JvmSymbolProtos.JvmVisibility.VISIBILITY_PRIVATE -> JvmVisibility.PRIVATE + JvmSymbolProtos.JvmVisibility.VISIBILITY_PACKAGE_PRIVATE -> JvmVisibility.PACKAGE_PRIVATE + else -> JvmVisibility.PUBLIC + } +} 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..23bd938484 --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt @@ -0,0 +1,191 @@ +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.InMemoryIndex +import org.appdevforall.codeonthego.indexing.MergedIndex +import org.appdevforall.codeonthego.indexing.PersistentIndex +import org.appdevforall.codeonthego.indexing.api.indexQuery +import org.appdevforall.codeonthego.indexing.util.BackgroundIndexer +import java.io.Closeable + +/** + * Main entry point for JVM symbol indexing. + * + * Combines a persistent index (libraries) with an in-memory index + * (source files) behind a merged view. Source symbols take priority. + */ +class JvmSymbolIndex private constructor( + /** Persistent cache — stores every JAR ever indexed. */ + val libraryCache: PersistentIndex, + + /** Filtered view — only shows JARs on the current classpath. */ + val libraryView: FilteredIndex, + + /** In-memory index for source file symbols. */ + val sourceIndex: InMemoryIndex, + + /** Merged view: source (priority) + active libraries. */ + val merged: MergedIndex, + + /** 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" + const val INDEX_NAME_SOURCES = "jvm-sources" + + fun create( + context: Context, + dbName: String = DB_NAME_DEFAULT, + ): JvmSymbolIndex { + val cache = PersistentIndex( + descriptor = JvmSymbolDescriptor, + context = context, + dbName = dbName, + name = INDEX_NAME_LIBRARY, + ) + + val view = FilteredIndex(cache) + + val sources = InMemoryIndex( + descriptor = JvmSymbolDescriptor, + name = INDEX_NAME_SOURCES, + ) + + // Sources win over libraries + val merged = MergedIndex(sources, view) + val indexer = BackgroundIndexer(cache) + return JvmSymbolIndex( + libraryCache = cache, + libraryView = view, + sourceIndex = sources, + merged = merged, + 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) + + suspend fun updateSourceFile(sourceId: String, symbols: Sequence) { + sourceIndex.removeBySource(sourceId) + sourceIndex.insertAll(symbols) + } + + suspend fun removeSourceFile(sourceId: String) { + sourceIndex.removeBySource(sourceId) + } + + fun findByPrefix(prefix: String, limit: Int = 200): Flow = + merged.query(indexQuery { prefix("name", prefix); this.limit = limit }) + + fun findByPrefix( + prefix: String, kinds: Set, limit: Int = 200, + ): Flow = + merged.query(indexQuery { prefix("name", prefix); this.limit = 0 }) + .filter { it.kind in kinds } + .take(limit) + + fun findExtensionsFor( + receiverTypeFqName: String, namePrefix: String = "", limit: Int = 200, + ): Flow = merged.query(indexQuery { + eq("receiverType", receiverTypeFqName) + if (namePrefix.isNotEmpty()) prefix("name", namePrefix) + this.limit = limit + }) + + fun findTopLevelCallablesInPackage( + packageName: String, namePrefix: String = "", limit: Int = 200, + ): Flow = merged.query(indexQuery { + eq("package", packageName) + if (namePrefix.isNotEmpty()) prefix("name", namePrefix) + this.limit = 0 + }).filter { it.kind.isCallable && it.isTopLevel }.take(limit) + + fun findClassifiersInPackage( + packageName: String, namePrefix: String = "", limit: Int = 200, + ): Flow = merged.query(indexQuery { + eq("package", packageName) + if (namePrefix.isNotEmpty()) prefix("name", namePrefix) + this.limit = 0 + }).filter { it.kind.isClassifier }.take(limit) + + fun findMembersOf( + classFqName: String, namePrefix: String = "", limit: Int = 200, + ): Flow = merged.query(indexQuery { + eq("containingClass", classFqName) + if (namePrefix.isNotEmpty()) prefix("name", namePrefix) + this.limit = limit + }) + + suspend fun findByFqName(fqName: String): JvmSymbol? = merged.get(fqName) + + fun allPackages(): Flow = merged.distinctValues("package") + + suspend fun awaitLibraryIndexing() = libraryIndexer.awaitAll() + + override fun close() { + libraryIndexer.close() + merged.close() + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..b4160f8b98 --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt @@ -0,0 +1,428 @@ +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.org.objectweb.asm.AnnotationVisitor +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.ClassVisitor +import org.jetbrains.org.objectweb.asm.Opcodes +import org.slf4j.LoggerFactory +import java.io.InputStream +import java.nio.file.Path +import java.util.jar.JarFile +import kotlin.io.path.pathString +import kotlin.metadata.ClassKind +import kotlin.metadata.KmClass +import kotlin.metadata.KmClassifier +import kotlin.metadata.KmFunction +import kotlin.metadata.KmPackage +import kotlin.metadata.KmProperty +import kotlin.metadata.KmType +import kotlin.metadata.Modality +import kotlin.metadata.Visibility +import kotlin.metadata.declaresDefaultValue +import kotlin.metadata.isConst +import kotlin.metadata.isDelegated +import kotlin.metadata.isExpect +import kotlin.metadata.isExternal +import kotlin.metadata.isInfix +import kotlin.metadata.isInline +import kotlin.metadata.isLateinit +import kotlin.metadata.isNullable +import kotlin.metadata.isOperator +import kotlin.metadata.isSuspend +import kotlin.metadata.isTailrec +import kotlin.metadata.jvm.KotlinClassMetadata +import kotlin.metadata.jvm.Metadata +import kotlin.metadata.kind +import kotlin.metadata.modality +import kotlin.metadata.visibility + +/** + * Scans JAR files using Kotlin metadata to produce [JvmSymbol]s + * with full Kotlin semantics (extensions, suspend, inline, etc.). + * + * Skips non-Kotlin class files (no `@Metadata` annotation). + */ +object KotlinMetadataScanner { + + private val log = LoggerFactory.getLogger(KotlinMetadataScanner::class.java) + + fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { + val jar = try { + JarFile(jarPath.toFile()) + } catch (e: Exception) { + log.warn("Failed to open JAR: {}", jarPath, e) + return@flow + } + + jar.use { + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (!entry.name.endsWith(".class")) continue + if (entry.name == "module-info.class") continue + + try { + jar.getInputStream(entry).use { input -> + parseKotlinClass(input, sourceId)?.forEach { emit(it) } + } + } catch (e: Exception) { + log.debug("Failed to parse {}: {}", entry.name, e.message) + } + } + } + } + .flowOn(Dispatchers.IO) + + internal fun parseKotlinClass(input: InputStream, sourceId: String): List? { + val reader = ClassReader(input) + val collector = MetadataCollector() + reader.accept( + collector, + ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES + ) + + val header = collector.metadataHeader ?: return null + + val metadata = try { + KotlinClassMetadata.readStrict(header) + } catch (e: Exception) { + log.debug("Failed to read Kotlin metadata: {}", e.message) + return null + } + + return when (metadata) { + is KotlinClassMetadata.Class -> + extractFromClass(metadata.kmClass, sourceId) + + is KotlinClassMetadata.FileFacade -> + extractFromPackage(metadata.kmPackage, collector.packageName, sourceId) + + is KotlinClassMetadata.MultiFileClassPart -> + extractFromPackage(metadata.kmPackage, collector.packageName, sourceId) + + else -> null + } + } + + private fun extractFromClass( + klass: KmClass, sourceId: String, + ): List { + val symbols = mutableListOf() + val classFqName = klass.name.replace('/', '.') + val packageName = classFqName.substringBeforeLast('.', "") + val shortName = classFqName.substringAfterLast('.') + + val kind = when (klass.kind) { + ClassKind.INTERFACE -> JvmSymbolKind.INTERFACE + ClassKind.ENUM_CLASS -> JvmSymbolKind.ENUM + ClassKind.ANNOTATION_CLASS -> JvmSymbolKind.ANNOTATION_CLASS + ClassKind.OBJECT -> JvmSymbolKind.OBJECT + ClassKind.COMPANION_OBJECT -> JvmSymbolKind.COMPANION_OBJECT + ClassKind.CLASS -> JvmSymbolKind.CLASS + else -> JvmSymbolKind.CLASS + } + + val supertypes = klass.supertypes.mapNotNull { supertype -> + when (val c = supertype.classifier) { + is KmClassifier.Class -> c.name.replace('/', '.') + else -> null + } + } + + symbols.add( + JvmSymbol( + key = classFqName, + sourceId = sourceId, + fqName = classFqName, + shortName = shortName, + packageName = packageName, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + visibility = kmVisibility(klass.visibility), + data = JvmClassInfo( + supertypeFqNames = supertypes, + typeParameters = klass.typeParameters.map { it.name }, + isAbstract = klass.modality == Modality.ABSTRACT, + isFinal = klass.modality == Modality.FINAL, + kotlin = KotlinClassInfo( + isSealed = klass.modality == Modality.SEALED, + sealedSubclasses = klass.sealedSubclasses.map { it.replace('/', '.') }, + ), + ), + ) + ) + + for (fn in klass.functions) { + extractFunction(fn, classFqName, packageName, sourceId)?.let { symbols.add(it) } + } + + for (prop in klass.properties) { + extractProperty(prop, classFqName, packageName, sourceId)?.let { symbols.add(it) } + } + + if (kind == JvmSymbolKind.ENUM) { + klass.kmEnumEntries.forEachIndexed { ordinal, entry -> + symbols.add( + JvmSymbol( + key = "$classFqName.$entry", + sourceId = sourceId, + fqName = "$classFqName.$entry", + shortName = entry.name, + packageName = packageName, + kind = JvmSymbolKind.ENUM_ENTRY, + language = JvmSourceLanguage.KOTLIN, + data = JvmEnumEntryInfo( + containingClassFqName = classFqName, + ordinal = ordinal, + ), + ) + ) + } + } + + return symbols + } + + private fun extractFromPackage( + pkg: KmPackage, + packageName: String, + sourceId: String, + ): List { + val symbols = mutableListOf() + + for (fn in pkg.functions) { + extractFunction(fn, "", packageName, sourceId)?.let { symbols.add(it) } + } + + for (prop in pkg.properties) { + extractProperty(prop, "", packageName, sourceId)?.let { symbols.add(it) } + } + + for (alias in pkg.typeAliases) { + val fqName = if (packageName.isEmpty()) alias.name else "$packageName.${alias.name}" + symbols.add( + JvmSymbol( + key = fqName, + sourceId = sourceId, + fqName = fqName, + shortName = alias.name, + packageName = packageName, + kind = JvmSymbolKind.TYPE_ALIAS, + language = JvmSourceLanguage.KOTLIN, + visibility = kmVisibility(alias.visibility), + data = JvmTypeAliasInfo( + expandedTypeFqName = kmTypeToFqName(alias.expandedType), + expandedTypeDisplay = kmTypeToDisplay(alias.expandedType), + typeParameters = alias.typeParameters.map { it.name }, + ), + ) + ) + } + + return symbols + } + + private fun extractFunction( + fn: KmFunction, + containingClass: String, + packageName: String, + sourceId: String, + ): JvmSymbol? { + val vis = kmVisibility(fn.visibility) + if (vis == JvmVisibility.PRIVATE) return null + + val receiverType = fn.receiverParameterType + val isExtension = receiverType != null + val kind = if (isExtension) JvmSymbolKind.EXTENSION_FUNCTION else JvmSymbolKind.FUNCTION + + val parameters = fn.valueParameters.map { param -> + JvmParameterInfo( + name = param.name, + typeFqName = kmTypeToFqName(param.type), + typeDisplay = kmTypeToDisplay(param.type), + hasDefaultValue = param.declaresDefaultValue, + isVararg = param.varargElementType != null, + ) + } + + val baseFqName = if (containingClass.isNotEmpty()) + "$containingClass.${fn.name}" else "$packageName.${fn.name}" + val key = "$baseFqName(${parameters.joinToString(",") { it.typeFqName }})" + + val signatureDisplay = buildString { + append("(") + append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplay}" }) + append("): ") + append(kmTypeToDisplay(fn.returnType)) + } + + return JvmSymbol( + key = key, + sourceId = sourceId, + fqName = baseFqName, + shortName = fn.name, + packageName = packageName, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + visibility = vis, + data = JvmFunctionInfo( + containingClassFqName = containingClass, + returnTypeFqName = kmTypeToFqName(fn.returnType), + returnTypeDisplay = kmTypeToDisplay(fn.returnType), + parameterCount = parameters.size, + parameters = parameters, + signatureDisplay = signatureDisplay, + typeParameters = fn.typeParameters.map { it.name }, + kotlin = KotlinFunctionInfo( + receiverTypeFqName = receiverType?.let { kmTypeToFqName(it) } ?: "", + receiverTypeDisplay = receiverType?.let { kmTypeToDisplay(it) } ?: "", + isSuspend = fn.isSuspend, + isInline = fn.isInline, + isInfix = fn.isInfix, + isOperator = fn.isOperator, + isTailrec = fn.isTailrec, + isExternal = fn.isExternal, + isExpect = fn.isExpect, + isReturnTypeNullable = fn.returnType.isNullable, + ), + ), + ) + } + + private fun extractProperty( + prop: KmProperty, + containingClass: String, + packageName: String, + sourceId: String, + ): JvmSymbol? { + val vis = kmVisibility(prop.visibility) + if (vis == JvmVisibility.PRIVATE) return null + + val receiverType = prop.receiverParameterType + val isExtension = receiverType != null + val kind = if (isExtension) JvmSymbolKind.EXTENSION_PROPERTY else JvmSymbolKind.PROPERTY + + val fqName = if (containingClass.isNotEmpty()) + "$containingClass.${prop.name}" else "$packageName.${prop.name}" + + return JvmSymbol( + key = fqName, + sourceId = sourceId, + fqName = fqName, + shortName = prop.name, + packageName = packageName, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + visibility = vis, + data = JvmFieldInfo( + containingClassFqName = containingClass, + typeFqName = kmTypeToFqName(prop.returnType), + typeDisplay = kmTypeToDisplay(prop.returnType), + kotlin = KotlinPropertyInfo( + receiverTypeFqName = receiverType?.let { kmTypeToFqName(it) } ?: "", + receiverTypeDisplay = receiverType?.let { kmTypeToDisplay(it) } ?: "", + isConst = prop.isConst, + isLateinit = prop.isLateinit, + hasGetter = prop.getter != null, + hasSetter = prop.setter != null, + isDelegated = prop.isDelegated, + isTypeNullable = prop.returnType.isNullable, + ), + ), + ) + } + + private fun kmTypeToFqName(type: KmType): String = when (val c = type.classifier) { + is KmClassifier.Class -> c.name.replace('/', '.') + is KmClassifier.TypeAlias -> c.name.replace('/', '.') + is KmClassifier.TypeParameter -> "T${c.id}" + } + + private fun kmTypeToDisplay(type: KmType): String { + val base = kmTypeToFqName(type).substringAfterLast('.') + val args = type.arguments.mapNotNull { it.type?.let { t -> kmTypeToDisplay(t) } } + return buildString { + append(base) + if (args.isNotEmpty()) append("<${args.joinToString(", ")}>") + if (type.isNullable) append("?") + } + } + + private fun kmVisibility(vis: Visibility) = when (vis) { + Visibility.PUBLIC -> JvmVisibility.PUBLIC + Visibility.PROTECTED -> JvmVisibility.PROTECTED + Visibility.INTERNAL -> JvmVisibility.INTERNAL + Visibility.PRIVATE, Visibility.PRIVATE_TO_THIS, Visibility.LOCAL -> JvmVisibility.PRIVATE + } + + private class MetadataCollector : ClassVisitor(Opcodes.ASM9) { + var metadataHeader: Metadata? = null + var packageName = "" + + private var metadataKind: Int? = null + private var metadataVersion: IntArray? = null + private var data1: Array? = null + private var data2: Array? = null + private var extraString: String? = null + private var pn: String? = null + private var extraInt: Int? = null + + override fun visit( + version: Int, access: Int, name: String, + signature: String?, superName: String?, interfaces: Array?, + ) { + val lastSlash = name.lastIndexOf('/') + packageName = if (lastSlash >= 0) name.substring(0, lastSlash).replace('/', '.') else "" + } + + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + if (descriptor != "Lkotlin/Metadata;") return null + + return object : AnnotationVisitor(Opcodes.ASM9) { + override fun visit(name: String?, value: Any?) { + when (name) { + "k" -> metadataKind = value as? Int + "xi" -> extraInt = value as? Int + "xs" -> extraString = value as? String + "pn" -> pn = value as? String + } + } + + override fun visitArray(name: String?): AnnotationVisitor = + object : AnnotationVisitor(Opcodes.ASM9) { + private val values = mutableListOf() + override fun visit(n: String?, value: Any?) { + value?.let { values.add(it) } + } + + override fun visitEnd() { + when (name) { + "mv" -> metadataVersion = + values.filterIsInstance().toIntArray() + + "d1" -> data1 = values.filterIsInstance().toTypedArray() + "d2" -> data2 = values.filterIsInstance().toTypedArray() + } + } + } + + override fun visitEnd() { + val kind = metadataKind ?: return + metadataHeader = Metadata( + kind = kind, + metadataVersion = metadataVersion ?: intArrayOf(), + data1 = data1 ?: emptyArray(), + data2 = data2 ?: emptyArray(), + extraString = extraString ?: "", + packageName = pn ?: "", + extraInt = extraInt ?: 0, + ) + } + } + } + } +} diff --git a/lsp/jvm-symbol-models/build.gradle.kts b/lsp/jvm-symbol-models/build.gradle.kts new file mode 100644 index 0000000000..40417fdcbf --- /dev/null +++ b/lsp/jvm-symbol-models/build.gradle.kts @@ -0,0 +1,37 @@ +import com.google.protobuf.gradle.id +import com.itsaky.androidide.plugins.conf.configureProtoc + +plugins { + id("java-library") + id("org.jetbrains.kotlin.jvm") + alias(libs.plugins.google.protobuf) +} + +configureProtoc(protobuf = protobuf, protocVersion = libs.versions.protobuf.asProvider()) + +protobuf { + plugins { + id("kotlin-ext") { + artifact = "dev.hsbrysk:protoc-gen-kotlin-ext:${libs.versions.protoc.gen.kotlin.ext.get()}:jdk8@jar" + } + } + generateProtoTasks { + all().forEach { task -> + task.plugins { + id("kotlin-ext") { + outputSubDir = "kotlin" + } + } + task.builtins { + getByName("java") { + option("lite") + } + } + } + } +} + +dependencies { + api(libs.google.protobuf.java) + api(libs.google.protobuf.kotlin) +} diff --git a/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto b/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto new file mode 100644 index 0000000000..a925e45979 --- /dev/null +++ b/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto @@ -0,0 +1,210 @@ +syntax = "proto3"; + +package org.appdevforall.codeonthego.indexing.jvm; + +option java_package = "org.appdevforall.codeonthego.indexing.jvm.proto"; +option java_outer_classname = "JvmSymbolProtos"; +option java_multiple_files = false; + +message JvmSymbolData { + + string fq_name = 1; + string short_name = 2; + string package_name = 3; + string source_id = 4; + + JvmSymbolKind kind = 5; + JvmSourceLanguage language = 6; + JvmVisibility visibility = 7; + bool is_deprecated = 8; + + oneof data { + ClassData class_data = 20; + FunctionData function_data = 21; + FieldData field_data = 22; + EnumEntryData enum_entry_data = 23; + TypeAliasData type_alias_data = 24; + } +} + +message ClassData { + + // FQN of the enclosing class (empty for top-level classes) + string containing_class_fq_name = 1; + + // Direct supertypes + repeated string supertype_fq_names = 2; + + // Type parameters: ["T", "R : Comparable"] + repeated string type_parameters = 3; + + // Modifiers + bool is_abstract = 4; + bool is_final = 5; + bool is_inner = 6; + bool is_static = 7; // static nested class in Java + + KotlinClassData kotlin = 10; +} + +message KotlinClassData { + bool is_data = 1; + bool is_value = 2; // inline/value class + bool is_sealed = 3; + bool is_fun_interface = 4; + bool is_expect = 5; + bool is_actual = 6; + bool is_external = 7; + + // Sealed subclass FQNs (only for sealed classes/interfaces) + repeated string sealed_subclasses = 8; + + // Companion object name (empty if none or uses default "Companion") + string companion_object_name = 9; +} + +message FunctionData { + + // FQN of the containing class (empty for top-level functions) + string containing_class_fq_name = 1; + + // Return type + string return_type_fq_name = 2; + string return_type_display = 3; + + // Parameters + int32 parameter_count = 4; + repeated ParameterData parameters = 5; + + // Human-readable signature: "(count: Int, sep: String): String" + string signature_display = 6; + + // Type parameters: ["T", "R : Comparable"] + repeated string type_parameters = 7; + + // Modifiers + bool is_static = 8; + bool is_abstract = 9; + bool is_final = 10; + + KotlinFunctionData kotlin = 20; +} + +message ParameterData { + string name = 1; + string type_fq_name = 2; + string type_display = 3; + bool has_default_value = 4; + + bool is_crossinline = 5; + bool is_noinline = 6; + bool is_vararg = 7; +} + +message KotlinFunctionData { + // Extension receiver type + string receiver_type_fq_name = 1; + string receiver_type_display = 2; + + // Modifiers + bool is_suspend = 3; + bool is_inline = 4; + bool is_infix = 5; + bool is_operator = 6; + bool is_tailrec = 7; + bool is_external = 8; + bool is_expect = 9; + bool is_actual = 10; + + bool is_return_type_nullable = 11; +} + +message FieldData { + + // FQN of the containing class (empty for top-level properties) + string containing_class_fq_name = 1; + + // Type of the field/property + string type_fq_name = 2; + string type_display = 3; + + // Modifiers + bool is_static = 4; + bool is_final = 5; + + // Constant value (for compile-time constants, as string repr) + string constant_value = 6; + + KotlinPropertyData kotlin = 20; +} + +message KotlinPropertyData { + // Extension receiver type + string receiver_type_fq_name = 1; + string receiver_type_display = 2; + + bool is_const = 3; + bool is_lateinit = 4; + bool has_getter = 5; + bool has_setter = 6; + bool is_delegated = 7; + bool is_expect = 8; + bool is_actual = 9; + bool is_external = 10; + + bool is_type_nullable = 11; +} + +message EnumEntryData { + // FQN of the containing enum class + string containing_enum_fq_name = 1; + + // Ordinal position + int32 ordinal = 2; +} + +message TypeAliasData { + // The type this alias expands to + string expanded_type_fq_name = 1; + string expanded_type_display = 2; + + // Type parameters: ["T"] + repeated string type_parameters = 3; +} + +enum JvmSymbolKind { + KIND_UNKNOWN = 0; + KIND_CLASS = 1; + KIND_INTERFACE = 2; + KIND_ENUM = 3; + KIND_ENUM_ENTRY = 4; + KIND_ANNOTATION_CLASS = 5; + KIND_OBJECT = 6; + KIND_COMPANION_OBJECT = 7; + KIND_DATA_CLASS = 8; + KIND_VALUE_CLASS = 9; + KIND_SEALED_CLASS = 10; + KIND_SEALED_INTERFACE = 11; + KIND_FUNCTION = 12; + KIND_EXTENSION_FUNCTION = 13; + KIND_CONSTRUCTOR = 14; + KIND_PROPERTY = 15; + KIND_EXTENSION_PROPERTY = 16; + KIND_FIELD = 17; + KIND_TYPE_ALIAS = 18; +} + +enum JvmSourceLanguage { + LANGUAGE_UNKNOWN = 0; + LANGUAGE_JAVA = 1; + LANGUAGE_KOTLIN = 2; +} + +enum JvmVisibility { + VISIBILITY_UNKNOWN = 0; + VISIBILITY_PUBLIC = 1; + VISIBILITY_PROTECTED = 2; + VISIBILITY_INTERNAL = 3; + VISIBILITY_PRIVATE = 4; + VISIBILITY_PACKAGE_PRIVATE = 5; +} diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index 66f2a74f4b..54c0fa6206 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { kapt(projects.annotationProcessors) implementation(projects.lsp.api) + implementation(projects.lsp.jvmSymbolIndex) implementation(projects.lsp.models) implementation(projects.editorApi) implementation(projects.eventbusEvents) 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 3bd433a429..bf0e4d9ddd 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 @@ -43,6 +43,7 @@ import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams 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.utils.DocumentUtils import com.itsaky.androidide.utils.Environment @@ -55,6 +56,7 @@ 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.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -122,6 +124,11 @@ class KotlinLanguageServer : ILanguageServer { override fun setupWithProject(workspace: Workspace) { logger.info("setupWithProject called, initialized={}", initialized) + (ProjectManagerImpl.getInstance() + .indexingServiceManager + .getService(JvmIndexingService.ID) as? JvmIndexingService?) + ?.refresh() + val jdkHome = Environment.JAVA_HOME.toPath() val jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE val intellijPluginRoot = Paths.get( diff --git a/settings.gradle.kts b/settings.gradle.kts index dfb9c6f997..6165b7a722 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -144,7 +144,10 @@ include( ":xml-inflater", ":lsp:api", ":lsp:models", + ":lsp:indexing", ":lsp:java", + ":lsp:jvm-symbol-index", + ":lsp:jvm-symbol-models", ":lsp:kotlin", ":lsp:kotlin-core", ":lsp:kotlin-stdlib-generator", diff --git a/subprojects/projects/build.gradle.kts b/subprojects/projects/build.gradle.kts index 79054954dd..9d2683ae80 100644 --- a/subprojects/projects/build.gradle.kts +++ b/subprojects/projects/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { api(projects.eventbus) api(projects.eventbusEvents) + api(projects.lsp.indexing) api(projects.subprojects.projectModels) api(projects.subprojects.toolingApi) diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManagerImpl.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManagerImpl.kt index 32ab789a01..357fda6b78 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManagerImpl.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManagerImpl.kt @@ -53,6 +53,8 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withContext +import org.appdevforall.codeonthego.indexing.service.IndexingService +import org.appdevforall.codeonthego.indexing.service.IndexingServiceManager import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -75,8 +77,19 @@ import kotlin.io.path.pathString class ProjectManagerImpl : IProjectManager, EventReceiver { + + private var _indexingServiceManager: IndexingServiceManager? = null lateinit var projectPath: String + val indexingServiceManager: IndexingServiceManager + get() { + if (_indexingServiceManager == null) { + _indexingServiceManager = IndexingServiceManager() + } + + return _indexingServiceManager!! + } + @Volatile internal var pluginProjectCached: Boolean? = null @@ -89,7 +102,7 @@ class ProjectManagerImpl : override val projectDirPath: String get() = projectPath - override val projectSyncIssues: List? + override val projectSyncIssues: List get() = gradleBuild?.syncIssueList ?: emptyList() companion object { @@ -140,6 +153,10 @@ class ProjectManagerImpl : gradleBuild.syncIssueList, ) + withStopWatch("notify indexing services") { + indexingServiceManager.onProjectSynced() + } + withStopWatch("Setup project") { val indexerScope = CoroutineScope(Dispatchers.Default) val modulesFlow = @@ -232,6 +249,9 @@ class ProjectManagerImpl : this.workspace = null pluginProjectCached = null + _indexingServiceManager?.close() + _indexingServiceManager = null + (this.androidBuildVariants as? MutableMap?)?.clear() } From c35aa9ffdba547b36d4a39e2b9bdc18c9b76f046 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 7 Apr 2026 22:08:51 +0530 Subject: [PATCH 27/58] fix: metadata version is sometimes not parsed Signed-off-by: Akash Yadav --- .../codeonthego/indexing/jvm/KotlinMetadataScanner.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 b4160f8b98..691d50dd25 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 @@ -385,6 +385,11 @@ object KotlinMetadataScanner { return object : AnnotationVisitor(Opcodes.ASM9) { override fun visit(name: String?, value: Any?) { when (name) { + "mv" -> { + if (value is IntArray) { + metadataVersion = value.copyOf() + } + } "k" -> metadataKind = value as? Int "xi" -> extraInt = value as? Int "xs" -> extraString = value as? String From bede4dbe30be56554a505a425d7f5feba2ea7fc5 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 8 Apr 2026 11:36:07 +0530 Subject: [PATCH 28/58] fix: make JvmSymbolIndex exclusive to external libraries Signed-off-by: Akash Yadav --- .../indexing/jvm/JvmIndexingService.kt | 19 +++---- ...ymbolIndex.kt => JvmLibrarySymbolIndex.kt} | 57 +++++-------------- 2 files changed, 22 insertions(+), 54 deletions(-) rename lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/{JvmSymbolIndex.kt => JvmLibrarySymbolIndex.kt} (72%) 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/JvmIndexingService.kt index a7cb834f81..a85c2c271e 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/JvmIndexingService.kt @@ -8,7 +8,6 @@ import com.itsaky.androidide.projects.models.bootClassPaths import com.itsaky.androidide.tasks.cancelIfActive import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -20,25 +19,23 @@ import org.greenrobot.eventbus.ThreadMode import org.slf4j.LoggerFactory import java.nio.file.Path import java.nio.file.Paths -import kotlin.io.path.exists import kotlin.io.path.extension -import kotlin.io.path.pathString /** - * Well-known key for the JVM symbol index. + * Well-known key for the JVM library symbol index. * * Both the Kotlin and Java LSPs use this key to retrieve the * shared index from the [IndexRegistry]. */ -val JVM_SYMBOL_INDEX = IndexKey("jvm-symbols") +val JVM_LIBRARY_SYMBOL_INDEX = IndexKey("jvm-library-symbols") /** * [IndexingService] that scans classpath JARs/AARs and builds - * a [JvmSymbolIndex]. + * a [JvmLibrarySymbolIndex]. * * Thread safety: all methods are called from the * [IndexingServiceManager][org.appdevforall.codeonthego.indexing.service.IndexingServiceManager]'s - * coroutine scope. The [JvmSymbolIndex] handles its own internal thread safety. + * coroutine scope. The [JvmLibrarySymbolIndex] handles its own internal thread safety. */ class JvmIndexingService( private val context: Context, @@ -51,16 +48,16 @@ class JvmIndexingService( override val id = ID - override val providedKeys = listOf(JVM_SYMBOL_INDEX) + override val providedKeys = listOf(JVM_LIBRARY_SYMBOL_INDEX) - private var index: JvmSymbolIndex? = null + private var index: JvmLibrarySymbolIndex? = null private var indexingMutex = Mutex() private val coroutineScope = CoroutineScope(Dispatchers.Default) override suspend fun initialize(registry: IndexRegistry) { - val jvmIndex = JvmSymbolIndex.create(context) + val jvmIndex = JvmLibrarySymbolIndex.create(context) this.index = jvmIndex - registry.register(JVM_SYMBOL_INDEX, jvmIndex) + registry.register(JVM_LIBRARY_SYMBOL_INDEX, jvmIndex) log.info("JVM symbol index initialized") } 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/JvmLibrarySymbolIndex.kt similarity index 72% rename from lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt rename to lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibrarySymbolIndex.kt index 23bd938484..13f2df3a67 100644 --- 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/JvmLibrarySymbolIndex.kt @@ -5,32 +5,21 @@ 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.InMemoryIndex -import org.appdevforall.codeonthego.indexing.MergedIndex import org.appdevforall.codeonthego.indexing.PersistentIndex import org.appdevforall.codeonthego.indexing.api.indexQuery import org.appdevforall.codeonthego.indexing.util.BackgroundIndexer import java.io.Closeable /** - * Main entry point for JVM symbol indexing. - * - * Combines a persistent index (libraries) with an in-memory index - * (source files) behind a merged view. Source symbols take priority. + * An index of symbols from external Java libraries (JARs). */ -class JvmSymbolIndex private constructor( +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, - /** In-memory index for source file symbols. */ - val sourceIndex: InMemoryIndex, - - /** Merged view: source (priority) + active libraries. */ - val merged: MergedIndex, - /** Background indexer writing to the cache. */ val libraryIndexer: BackgroundIndexer, ) : Closeable { @@ -39,12 +28,11 @@ class JvmSymbolIndex private constructor( const val DB_NAME_DEFAULT = "jvm_symbol_index.db" const val INDEX_NAME_LIBRARY = "jvm-library-cache" - const val INDEX_NAME_SOURCES = "jvm-sources" fun create( context: Context, dbName: String = DB_NAME_DEFAULT, - ): JvmSymbolIndex { + ): JvmLibrarySymbolIndex { val cache = PersistentIndex( descriptor = JvmSymbolDescriptor, context = context, @@ -54,19 +42,10 @@ class JvmSymbolIndex private constructor( val view = FilteredIndex(cache) - val sources = InMemoryIndex( - descriptor = JvmSymbolDescriptor, - name = INDEX_NAME_SOURCES, - ) - - // Sources win over libraries - val merged = MergedIndex(sources, view) val indexer = BackgroundIndexer(cache) - return JvmSymbolIndex( + return JvmLibrarySymbolIndex( libraryCache = cache, libraryView = view, - sourceIndex = sources, - merged = merged, libraryIndexer = indexer ) } @@ -127,28 +106,19 @@ class JvmSymbolIndex private constructor( provider: (sourceId: String) -> Flow, ) = libraryIndexer.indexSource(sourceId, skipIfExists = false, provider) - suspend fun updateSourceFile(sourceId: String, symbols: Sequence) { - sourceIndex.removeBySource(sourceId) - sourceIndex.insertAll(symbols) - } - - suspend fun removeSourceFile(sourceId: String) { - sourceIndex.removeBySource(sourceId) - } - fun findByPrefix(prefix: String, limit: Int = 200): Flow = - merged.query(indexQuery { prefix("name", prefix); this.limit = limit }) + libraryView.query(indexQuery { prefix("name", prefix); this.limit = limit }) fun findByPrefix( prefix: String, kinds: Set, limit: Int = 200, ): Flow = - merged.query(indexQuery { prefix("name", prefix); this.limit = 0 }) + libraryView.query(indexQuery { prefix("name", prefix); this.limit = 0 }) .filter { it.kind in kinds } .take(limit) fun findExtensionsFor( receiverTypeFqName: String, namePrefix: String = "", limit: Int = 200, - ): Flow = merged.query(indexQuery { + ): Flow = libraryView.query(indexQuery { eq("receiverType", receiverTypeFqName) if (namePrefix.isNotEmpty()) prefix("name", namePrefix) this.limit = limit @@ -156,7 +126,7 @@ class JvmSymbolIndex private constructor( fun findTopLevelCallablesInPackage( packageName: String, namePrefix: String = "", limit: Int = 200, - ): Flow = merged.query(indexQuery { + ): Flow = libraryView.query(indexQuery { eq("package", packageName) if (namePrefix.isNotEmpty()) prefix("name", namePrefix) this.limit = 0 @@ -164,7 +134,7 @@ class JvmSymbolIndex private constructor( fun findClassifiersInPackage( packageName: String, namePrefix: String = "", limit: Int = 200, - ): Flow = merged.query(indexQuery { + ): Flow = libraryView.query(indexQuery { eq("package", packageName) if (namePrefix.isNotEmpty()) prefix("name", namePrefix) this.limit = 0 @@ -172,20 +142,21 @@ class JvmSymbolIndex private constructor( fun findMembersOf( classFqName: String, namePrefix: String = "", limit: Int = 200, - ): Flow = merged.query(indexQuery { + ): Flow = libraryView.query(indexQuery { eq("containingClass", classFqName) if (namePrefix.isNotEmpty()) prefix("name", namePrefix) this.limit = limit }) - suspend fun findByFqName(fqName: String): JvmSymbol? = merged.get(fqName) + suspend fun findByFqName(fqName: String): JvmSymbol? = libraryView.get(fqName) - fun allPackages(): Flow = merged.distinctValues("package") + fun allPackages(): Flow = libraryView.distinctValues("package") suspend fun awaitLibraryIndexing() = libraryIndexer.awaitAll() override fun close() { + libraryCache.close() libraryIndexer.close() - merged.close() + libraryView.close() } } \ No newline at end of file From 9eb4ffebe85c812d90983ff074471100b7798d85 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 8 Apr 2026 16:52:54 +0530 Subject: [PATCH 29/58] feat: add module resolver to resolve library modules from source path Signed-off-by: Akash Yadav --- .../language/treesitter/TreeSitterLanguage.kt | 312 +++++++++--------- .../indexing/jvm/JvmLibrarySymbolIndex.kt | 26 +- .../indexing/jvm/JvmSymbolDescriptor.kt | 33 +- .../kotlin/compiler/CompilationEnvironment.kt | 10 +- .../lsp/kotlin/compiler/Compiler.kt | 4 +- .../lsp/kotlin/compiler/KotlinProjectModel.kt | 57 +++- .../lsp/kotlin/compiler/ModuleResolver.kt | 25 ++ .../kotlin/completion/KotlinCompletions.kt | 12 +- .../completion/SymbolVisibilityChecker.kt | 87 +++++ .../diagnostic/KotlinDiagnosticProvider.kt | 3 +- 10 files changed, 367 insertions(+), 202 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ModuleResolver.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/SymbolVisibilityChecker.kt diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt index 4c7d677fc3..bd641f3a64 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt @@ -27,6 +27,7 @@ import com.itsaky.androidide.editor.schemes.LanguageSpecProvider.getLanguageSpec import com.itsaky.androidide.editor.schemes.LocalCaptureSpecProvider.newLocalCaptureSpec import com.itsaky.androidide.editor.utils.isNonBlankLine import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TreeSitter import com.itsaky.androidide.utils.IntPair import io.github.rosemoe.sora.editor.ts.TsTheme import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG @@ -42,156 +43,167 @@ import java.io.File * @author Akash Yadav */ abstract class TreeSitterLanguage( - context: Context, - lang: TSLanguage, - private val langType: String + context: Context, + lang: TSLanguage, + private val langType: String ) : IDELanguage() { - private var languageSpec = - getLanguageSpec(context, langType, lang, newLocalCaptureSpec(langType)) - private var tsTheme = TsTheme(languageSpec.spec.tsQuery) - private lateinit var _indentProvider: TreeSitterIndentProvider - private val analyzer by lazy { TreeSitterAnalyzeManager(languageSpec.spec, tsTheme) } - private val newlineHandlersLazy by lazy { createNewlineHandlers() } - - private var languageScheme: LanguageScheme? = null - - private val indentProvider: TreeSitterIndentProvider - get() { - if (!this::_indentProvider.isInitialized) { - this._indentProvider = TreeSitterIndentProvider( - languageSpec, - analyzer.analyzeWorker!!, - getTabSize() - ) - } - - return _indentProvider - } - - companion object { - - private val log = LoggerFactory.getLogger(TreeSitterLanguage::class.java) - private const val DEF_IDENT_ADV = 0 - } - - fun setupWith(scheme: IDEColorScheme?) { - val langScheme = scheme?.languages?.get(langType) - this.languageScheme = langScheme - this.analyzer.langScheme = languageScheme - langScheme?.styles?.forEach { tsTheme.putStyleRule(it.key, it.value.makeStyle()) } - } - - override fun addBreakpoint(line: Int) { - this.analyzer.addBreakpoint(line) - } - - override fun removeBreakpoint(line: Int) { - this.analyzer.removeBreakpoint(line) - } - - override fun removeAllBreakpoints() { - this.analyzer.removeAllBreakpoints() - } - - override fun toggleBreakpoint(line: Int) { - this.analyzer.toggleBreakpoint(line) - } - - override fun highlightLine(line: Int) { - this.analyzer.highlightLine(line) - } - - override fun unhighlightLines() { - this.analyzer.unhighlightLines() - } - - override fun getAnalyzeManager(): AnalyzeManager { - return this.analyzer - } - - override fun getSymbolPairs(): SymbolPairMatch { - return CommonSymbolPairs() - } - - open fun createNewlineHandlers(): Array { - return emptyArray() - } - - override fun getNewlineHandlers(): Array { - return newlineHandlersLazy - } - - override fun getInterruptionLevel(): Int { - return INTERRUPTION_LEVEL_STRONG - } - - override fun getIndentAdvance( - content: ContentReference, - line: Int, - column: Int, - spaceCountOnLine: Int, - tabCountOnLine: Int - ): Int { - return try { - if (line == content.reference.lineCount - 1) { - // line + 1 does not exist - // TODO(itsaky): Update this implementation when this behavior is fixed in sora-editor - return DEF_IDENT_ADV - } - - var linesToReq = LongArray(1) - linesToReq[0] = IntPair.pack(line, column) - - if (content.reference.isNonBlankLine(line + 1)) { - // consider the indentation of the next line only if it is non-blank - linesToReq += IntPair.pack(line + 1, 0) - } - - val indents = this.indentProvider.getIndentsForLines( - content = content.reference, - positions = linesToReq, - ) - - if (indents.size == 1) { - val indent = indents[0] - if (indent == TreeSitterIndentProvider.INDENTATION_ERR) { - return DEF_IDENT_ADV - } - - return indent - (spaceCountOnLine + (tabCountOnLine * getTabSize())) - } - - val (indentLine, indentNxtLine) = indents - if (indentLine == TreeSitterIndentProvider.INDENTATION_ERR - || indentNxtLine == TreeSitterIndentProvider.INDENTATION_ERR) { - log.debug( - "expectedIndent[{}]={}, expectedIndentNextLine[{}]={}, returning default indent advance", - line, indentLine, line + 1, indentNxtLine) - return DEF_IDENT_ADV - } - - return indentNxtLine - indentLine - } catch (e: Exception) { - log.error("An error occurred computing indentation at line:column::{}:{}", line, column, e) - DEF_IDENT_ADV - } - - } - - override fun destroy() { - this.languageSpec.close() - this.languageScheme = null - } - - /** A [Factory] creates instance of a specific [TreeSitterLanguage] implementation. */ - fun interface Factory { - - /** - * Create the instance of the [TreeSitterLanguage] implementation. - * - * @param context The current context. - */ - fun create(context: Context): T - } + private var languageSpec = + getLanguageSpec(context, langType, lang, newLocalCaptureSpec(langType)) + private var tsTheme = TsTheme(languageSpec.spec.tsQuery) + private lateinit var _indentProvider: TreeSitterIndentProvider + private val analyzer by lazy { TreeSitterAnalyzeManager(languageSpec.spec, tsTheme) } + private val newlineHandlersLazy by lazy { createNewlineHandlers() } + + private var languageScheme: LanguageScheme? = null + + private val indentProvider: TreeSitterIndentProvider + get() { + if (!this::_indentProvider.isInitialized) { + this._indentProvider = TreeSitterIndentProvider( + languageSpec, + analyzer.analyzeWorker!!, + getTabSize() + ) + } + + return _indentProvider + } + + companion object { + + init { + TreeSitter.loadLibrary() + } + + private val log = LoggerFactory.getLogger(TreeSitterLanguage::class.java) + private const val DEF_IDENT_ADV = 0 + } + + fun setupWith(scheme: IDEColorScheme?) { + val langScheme = scheme?.languages?.get(langType) + this.languageScheme = langScheme + this.analyzer.langScheme = languageScheme + langScheme?.styles?.forEach { tsTheme.putStyleRule(it.key, it.value.makeStyle()) } + } + + override fun addBreakpoint(line: Int) { + this.analyzer.addBreakpoint(line) + } + + override fun removeBreakpoint(line: Int) { + this.analyzer.removeBreakpoint(line) + } + + override fun removeAllBreakpoints() { + this.analyzer.removeAllBreakpoints() + } + + override fun toggleBreakpoint(line: Int) { + this.analyzer.toggleBreakpoint(line) + } + + override fun highlightLine(line: Int) { + this.analyzer.highlightLine(line) + } + + override fun unhighlightLines() { + this.analyzer.unhighlightLines() + } + + override fun getAnalyzeManager(): AnalyzeManager { + return this.analyzer + } + + override fun getSymbolPairs(): SymbolPairMatch { + return CommonSymbolPairs() + } + + open fun createNewlineHandlers(): Array { + return emptyArray() + } + + override fun getNewlineHandlers(): Array { + return newlineHandlersLazy + } + + override fun getInterruptionLevel(): Int { + return INTERRUPTION_LEVEL_STRONG + } + + override fun getIndentAdvance( + content: ContentReference, + line: Int, + column: Int, + spaceCountOnLine: Int, + tabCountOnLine: Int + ): Int { + return try { + if (line == content.reference.lineCount - 1) { + // line + 1 does not exist + // TODO(itsaky): Update this implementation when this behavior is fixed in sora-editor + return DEF_IDENT_ADV + } + + var linesToReq = LongArray(1) + linesToReq[0] = IntPair.pack(line, column) + + if (content.reference.isNonBlankLine(line + 1)) { + // consider the indentation of the next line only if it is non-blank + linesToReq += IntPair.pack(line + 1, 0) + } + + val indents = this.indentProvider.getIndentsForLines( + content = content.reference, + positions = linesToReq, + ) + + if (indents.size == 1) { + val indent = indents[0] + if (indent == TreeSitterIndentProvider.INDENTATION_ERR) { + return DEF_IDENT_ADV + } + + return indent - (spaceCountOnLine + (tabCountOnLine * getTabSize())) + } + + val (indentLine, indentNxtLine) = indents + if (indentLine == TreeSitterIndentProvider.INDENTATION_ERR + || indentNxtLine == TreeSitterIndentProvider.INDENTATION_ERR + ) { + log.debug( + "expectedIndent[{}]={}, expectedIndentNextLine[{}]={}, returning default indent advance", + line, indentLine, line + 1, indentNxtLine + ) + return DEF_IDENT_ADV + } + + return indentNxtLine - indentLine + } catch (e: Exception) { + log.error( + "An error occurred computing indentation at line:column::{}:{}", + line, + column, + e + ) + DEF_IDENT_ADV + } + + } + + override fun destroy() { + this.languageSpec.close() + this.languageScheme = null + } + + /** A [Factory] creates instance of a specific [TreeSitterLanguage] implementation. */ + fun interface Factory { + + /** + * Create the instance of the [TreeSitterLanguage] implementation. + * + * @param context The current context. + */ + fun create(context: Context): T + } } 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 index 13f2df3a67..ec52e5d633 100644 --- 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 @@ -7,6 +7,10 @@ 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 @@ -107,50 +111,50 @@ class JvmLibrarySymbolIndex private constructor( ) = libraryIndexer.indexSource(sourceId, skipIfExists = false, provider) fun findByPrefix(prefix: String, limit: Int = 200): Flow = - libraryView.query(indexQuery { prefix("name", prefix); this.limit = limit }) + libraryView.query(indexQuery { prefix(KEY_NAME, prefix); this.limit = limit }) fun findByPrefix( prefix: String, kinds: Set, limit: Int = 200, ): Flow = - libraryView.query(indexQuery { prefix("name", prefix); this.limit = 0 }) + 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("receiverType", receiverTypeFqName) - if (namePrefix.isNotEmpty()) prefix("name", namePrefix) + 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("package", packageName) - if (namePrefix.isNotEmpty()) prefix("name", namePrefix) + 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("package", packageName) - if (namePrefix.isNotEmpty()) prefix("name", namePrefix) + 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("containingClass", classFqName) - if (namePrefix.isNotEmpty()) prefix("name", namePrefix) + 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("package") + fun allPackages(): Flow = libraryView.distinctValues(KEY_PACKAGE) suspend fun awaitLibraryIndexing() = libraryIndexer.awaitAll() diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt index 949240eafd..4d34d1b55d 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt @@ -16,28 +16,35 @@ import org.appdevforall.codeonthego.indexing.jvm.proto.JvmSymbolProtos.JvmSymbol * - `containingClass`: exact, for member lookup * - `language` : exact, for Java-only or Kotlin-only queries * - * Blob serialization uses Protobuf with oneof for type-specific data. + * Blob serialization uses Protobuf with `oneof` for type-specific data. */ object JvmSymbolDescriptor : IndexDescriptor { + const val KEY_NAME = "name" + const val KEY_PACKAGE = "package" + const val KEY_KIND = "kind" + const val KEY_RECEIVER_TYPE = "receiverType" + const val KEY_CONTAINING_CLASS = "containingClass" + const val KEY_LANGUAGE = "language" + override val name: String = "jvm_symbols" override val fields: List = listOf( - IndexField(name = "name", prefixSearchable = true), - IndexField(name = "package"), - IndexField(name = "kind"), - IndexField(name = "receiverType"), - IndexField(name = "containingClass"), - IndexField(name = "language"), + IndexField(name = KEY_NAME, prefixSearchable = true), + IndexField(name = KEY_PACKAGE), + IndexField(name = KEY_KIND), + IndexField(name = KEY_RECEIVER_TYPE), + IndexField(name = KEY_CONTAINING_CLASS), + IndexField(name = KEY_LANGUAGE), ) override fun fieldValues(entry: JvmSymbol): Map = mapOf( - "name" to entry.shortName, - "package" to entry.packageName, - "kind" to entry.kind.name, - "receiverType" to entry.receiverTypeFqName, - "containingClass" to entry.containingClassFqName.ifEmpty { null }, - "language" to entry.language.name, + KEY_NAME to entry.shortName, + KEY_PACKAGE to entry.packageName, + KEY_KIND to entry.kind.name, + KEY_RECEIVER_TYPE to entry.receiverTypeFqName, + KEY_CONTAINING_CLASS to entry.containingClassFqName.ifEmpty { null }, + KEY_LANGUAGE to entry.language.name, ) override fun serialize(entry: JvmSymbol): ByteArray = 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 f9b20ebc3a..6bb19535d9 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,7 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler -import com.itsaky.androidide.lsp.kotlin.FileEventConsumer import com.itsaky.androidide.lsp.kotlin.KtFileManager +import com.itsaky.androidide.lsp.kotlin.completion.SymbolVisibilityChecker import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory @@ -51,7 +51,7 @@ import kotlin.io.path.pathString * @param jdkHome Path to the JDK installation directory. * @param jdkRelease The JDK release version at [jdkHome]. */ -class CompilationEnvironment( +internal class CompilationEnvironment( val projectModel: KotlinProjectModel, val intellijPluginRoot: Path, val jdkHome: Path, @@ -82,6 +82,12 @@ class CompilationEnvironment( val coreApplicationEnvironment: CoreApplicationEnvironment get() = session.coreApplicationEnvironment + val moduleResolver: ModuleResolver? + get() = projectModel.moduleResolver + + val symbolVisibilityChecker: SymbolVisibilityChecker? + get() = projectModel.symbolVisibilityChecker + private val envMessageCollector = object : MessageCollector { override fun clear() { } 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 a02e6ebe44..48bde185b6 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,6 +1,5 @@ package com.itsaky.androidide.lsp.kotlin.compiler -import com.itsaky.androidide.lsp.kotlin.FileEventConsumer import com.itsaky.androidide.utils.DocumentUtils import org.jetbrains.kotlin.com.intellij.lang.Language import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems @@ -15,10 +14,9 @@ import org.jetbrains.kotlin.psi.KtPsiFactory import org.slf4j.LoggerFactory import java.nio.file.Path import java.nio.file.Paths -import kotlin.io.path.extension import kotlin.io.path.pathString -class Compiler( +internal class Compiler( projectModel: KotlinProjectModel, intellijPluginRoot: Path, jdkHome: Path, 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 e78b8646c1..06bd635704 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,17 +1,20 @@ package com.itsaky.androidide.lsp.kotlin.compiler +import com.itsaky.androidide.lsp.kotlin.completion.SymbolVisibilityChecker 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.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.jetbrains.kotlin.config.LanguageVersion 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]. @@ -24,15 +27,23 @@ import org.slf4j.LoggerFactory * (build complete), it notifies registered listeners so they can * refresh their sessions. */ -class KotlinProjectModel { +internal class KotlinProjectModel { private val logger = LoggerFactory.getLogger(KotlinProjectModel::class.java) 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 + /** * The kind of change that occurred. */ @@ -93,49 +104,55 @@ class KotlinProjectModel { 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 { bootClassPath -> - addModule(buildKtLibraryModule { - this.platform = this@KotlinProjectModel.platform - this.libraryName = bootClassPath.nameWithoutExtension - addBinaryRoot(bootClassPath.toPath()) - }) - } + .map { it.toPath() } + .map(::addLibrary) } val libraryDependencies = moduleProjects .flatMap { it.getCompileClasspaths() } .filter { it.exists() } - .associateWith { library -> - addModule(buildKtLibraryModule { - this.platform = this@KotlinProjectModel.platform - this.libraryName = library.nameWithoutExtension - addBinaryRoot(library.toPath()) - }) - } + .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(project.getSourceDirectories().map { it.toPath() }) + addSourceRoots(sourceRoots) bootClassPaths.forEach { addRegularDependency(it) } project.getCompileClasspaths(excludeSourceGeneratedClassPath = true) .forEach { classpath -> - val libDep = libraryDependencies[classpath] + val libDep = libraryDependencies[classpath.toPath()] if (libDep == null) { logger.error( "Skipping non-existent classpath classpath: {}", @@ -156,6 +173,10 @@ class KotlinProjectModel { } moduleProjects.forEach { addModule(getOrCreateModule(it)) } + + val moduleResolver = ModuleResolver(jarMap = jarToModMap) + _moduleResolver = moduleResolver + _symbolVisibilityChecker = SymbolVisibilityChecker(moduleResolver) } } 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 new file mode 100644 index 0000000000..704d02978a --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ModuleResolver.kt @@ -0,0 +1,25 @@ +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/completion/KotlinCompletions.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt index 51613960b2..ca003ee1c8 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 @@ -38,12 +38,10 @@ import org.jetbrains.kotlin.analysis.api.types.KaClassType import org.jetbrains.kotlin.analysis.api.types.KaType import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType -import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.types.Variance import org.slf4j.LoggerFactory @@ -58,7 +56,7 @@ private val logger = LoggerFactory.getLogger("KotlinCompletions") * @param params The completion parameters. * @return The completion result. */ -fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult { +internal fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult { val managedFile = fileManager.getOpenFile(params.file) if (managedFile == null) { logger.warn("No managed file for {}", params.file) @@ -91,6 +89,12 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult useSiteElement = completionKtFile, resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, ) { + val symbolVisibilityChecker = this@complete.symbolVisibilityChecker + if (symbolVisibilityChecker == null) { + logger.error("No symbol visibility checker available!") + return@analyzeCopy CompletionResult.EMPTY + } + val cursorContext = resolveCursorContext(completionKtFile, completionOffset) if (cursorContext == null) { logger.error( @@ -117,6 +121,7 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult collectScopeCompletions( scopeContext = scopeContext, scope = compositeScope, + symbolVisibilityChecker = symbolVisibilityChecker, ktElement = ktElement, partial = partial, to = items @@ -240,6 +245,7 @@ private fun KaSession.collectExtensionFunctions( private fun KaSession.collectScopeCompletions( scopeContext: KaScopeContext, scope: KaScope, + symbolVisibilityChecker: SymbolVisibilityChecker, ktElement: KtElement, partial: String, to: MutableList, 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 new file mode 100644 index 0000000000..010b187e41 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/SymbolVisibilityChecker.kt @@ -0,0 +1,87 @@ +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 ac2c38672f..4472c1a652 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 @@ -9,7 +9,6 @@ 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 @@ -23,7 +22,7 @@ import kotlin.time.toKotlinInstant private val logger = LoggerFactory.getLogger("KotlinDiagnosticProvider") -fun CompilationEnvironment.collectDiagnosticsFor(file: Path): DiagnosticResult = try { +internal fun CompilationEnvironment.collectDiagnosticsFor(file: Path): DiagnosticResult = try { logger.info("Analyzing file: {}", file) return doAnalyze(file) } catch (err: Throwable) { From 1fdd5b349a8cbac886aff587fde9ee4ce663a985 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 8 Apr 2026 19:26:31 +0530 Subject: [PATCH 30/58] feat: add support for completing non-imported symbols Signed-off-by: Akash Yadav --- .../editor/adapters/CompletionListAdapter.kt | 348 +++++++++--------- .../language/CommonCompletionProvider.kt | 96 ++--- .../androidide/editor/language/IDELanguage.kt | 184 ++++----- .../lsp/kotlin/KotlinLanguageServer.kt | 8 +- .../kotlin/compiler/CompilationEnvironment.kt | 13 +- .../lsp/kotlin/compiler/Compiler.kt | 2 +- .../lsp/kotlin/compiler/KotlinProjectModel.kt | 9 + .../kotlin/completion/KotlinCompletions.kt | 161 ++++++-- .../lsp/edits/DefaultEditHandler.kt | 211 ++++++----- 9 files changed, 583 insertions(+), 449 deletions(-) diff --git a/editor/src/main/java/com/itsaky/androidide/editor/adapters/CompletionListAdapter.kt b/editor/src/main/java/com/itsaky/androidide/editor/adapters/CompletionListAdapter.kt index a43471c8cc..67b3bfe11f 100755 --- a/editor/src/main/java/com/itsaky/androidide/editor/adapters/CompletionListAdapter.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/adapters/CompletionListAdapter.kt @@ -25,6 +25,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.core.view.isVisible import com.itsaky.androidide.editor.R import com.itsaky.androidide.editor.databinding.LayoutCompletionItemBinding import com.itsaky.androidide.lookup.Lookup @@ -55,177 +56,178 @@ import com.itsaky.androidide.lsp.models.CompletionItem as LspCompletionItem class CompletionListAdapter : EditorCompletionAdapter() { - override fun getItemHeight(): Int { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 40f, - Resources.getSystem().displayMetrics - ) - .toInt() - } - - override fun getView( - position: Int, - convertView: View?, - parent: ViewGroup?, - isCurrentCursorPosition: Boolean, - ): View { - val binding = - convertView?.let { LayoutCompletionItemBinding.bind(it) } - ?: LayoutCompletionItemBinding.inflate(LayoutInflater.from(context), parent, false) - val item = getItem(position) as LspCompletionItem - val label = item.ideLabel - val desc = item.detail - var type: String? = item.completionKind.toString() - val header = if (type!!.isEmpty()) "O" else type[0].toString() - if (item.overrideTypeText != null) { - type = item.overrideTypeText - } - binding.completionIconText.text = header - binding.completionLabel.text = label - binding.completionType.text = type - binding.completionDetail.text = desc - binding.completionIconText.setTypeface(customOrJBMono(EditorPreferences.useCustomFont), - Typeface.BOLD) - if (desc.isEmpty()) { - binding.completionDetail.visibility = View.GONE - } - - binding.completionApiInfo.visibility = View.GONE - - applyColorScheme(binding, isCurrentCursorPosition) - showApiInfoIfNeeded(item, binding.completionApiInfo) - return binding.root - } - - private fun applyColorScheme(binding: LayoutCompletionItemBinding, isCurrent: Boolean) { - setItemBackground(binding, isCurrent) - var color = getThemeColor(COMPLETION_WND_TEXT_LABEL) - if (color != 0) { - binding.completionLabel.setTextColor(color) - binding.completionIconText.setTextColor(color) - } - - color = getThemeColor(COMPLETION_WND_TEXT_DETAIL) - if (color != 0) { - binding.completionDetail.setTextColor(color) - } - - color = getThemeColor(COMPLETION_WND_TEXT_API) - if (color != 0) { - binding.completionApiInfo.setTextColor(color) - } - - color = getThemeColor(COMPLETION_WND_TEXT_TYPE) - if (color != 0) { - binding.completionType.setTextColor(color) - } - } - - private fun setItemBackground(binding: LayoutCompletionItemBinding, isCurrent: Boolean) { - val color = - if (isCurrent) getThemeColor(SchemeAndroidIDE.COMPLETION_WND_BG_CURRENT_ITEM) - else 0 - - val cornerRadius = binding.root.context.resources - .getDimensionPixelSize(R.dimen.completion_window_corner_radius).toFloat() - - val gd = GradientDrawable().apply { - setColor(color) - setCornerRadius(cornerRadius) - } - - binding.root.background = gd - } - - private fun showApiInfoIfNeeded(item: LspCompletionItem, textView: TextView) { - executeAsync({ - if (!isValidForApiVersion(item)) { - return@executeAsync null - } - - val data = item.data - val versions = - Lookup.getDefault().lookup(ApiVersions.COMPLETION_LOOKUP_KEY) ?: return@executeAsync null - val className = - when (data) { - is ClassCompletionData -> data.className - is MemberCompletionData -> data.classInfo.className - else -> return@executeAsync null - } - val kind = item.completionKind - - val clazz = versions.getClass(className) ?: return@executeAsync null - var info: Info? = clazz - - if (data is MethodCompletionData) { - if ( - kind == METHOD && data.erasedParameterTypes.isNotEmpty() && data.memberName.isNotBlank() - ) { - val method = clazz.getMethod(data.memberName, *data.erasedParameterTypes.toTypedArray()) - if (method != null) { - info = method - } - } else if (kind == FIELD && data.memberName.isNotBlank()) { - val field = clazz.getField(data.memberName) - if (field != null) { - info = field - } - } - } - val sb = StringBuilder() - if (info!!.since > 1) { - sb.append(textView.context.getString(msg_api_info_since, info.since)) - sb.append("\n") - } - - if (info.removed > 0) { - sb.append(textView.context.getString(msg_api_info_removed, info.removed)) - sb.append("\n") - } - - if (info.deprecated > 0) { - sb.append(textView.context.getString(msg_api_info_deprecated, info.deprecated)) - sb.append("\n") - } - - return@executeAsync sb - }) { - if (it.isNullOrBlank()) { - textView.visibility = View.GONE - return@executeAsync - } - - textView.text = it - textView.visibility = View.VISIBLE - } - } - - private fun isValidForApiVersion(item: LspCompletionItem?): Boolean { - if (item == null) { - return false - } - val type = item.completionKind - val data = item.data - return if ( // These represent a class type - (type === CLASS || - type === INTERFACE || - type === ENUM || - - // These represent a method type - type === METHOD || - type === CONSTRUCTOR || - - // A field type - type === FIELD) && data != null - ) { - val className = - when (data) { - is ClassCompletionData -> data.className - is MemberCompletionData -> data.classInfo.className - else -> null - } - !TextUtils.isEmpty(className) - } else false - } + override fun getItemHeight(): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 40f, + Resources.getSystem().displayMetrics + ) + .toInt() + } + + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup?, + isCurrentCursorPosition: Boolean, + ): View { + val binding = + convertView?.let { LayoutCompletionItemBinding.bind(it) } + ?: LayoutCompletionItemBinding.inflate(LayoutInflater.from(context), parent, false) + val item = getItem(position) as LspCompletionItem + val label = item.ideLabel + val desc = item.detail + var type: String? = item.completionKind.toString() + val header = if (type!!.isEmpty()) "O" else type[0].toString() + if (item.overrideTypeText != null) { + type = item.overrideTypeText + } + binding.completionIconText.text = header + binding.completionLabel.text = label + binding.completionType.text = type + binding.completionDetail.text = desc + binding.completionIconText.setTypeface( + customOrJBMono(EditorPreferences.useCustomFont), + Typeface.BOLD + ) + binding.completionApiInfo.visibility = View.GONE + binding.completionDetail.isVisible = desc.isNotEmpty() + + applyColorScheme(binding, isCurrentCursorPosition) + showApiInfoIfNeeded(item, binding.completionApiInfo) + return binding.root + } + + private fun applyColorScheme(binding: LayoutCompletionItemBinding, isCurrent: Boolean) { + setItemBackground(binding, isCurrent) + var color = getThemeColor(COMPLETION_WND_TEXT_LABEL) + if (color != 0) { + binding.completionLabel.setTextColor(color) + binding.completionIconText.setTextColor(color) + } + + color = getThemeColor(COMPLETION_WND_TEXT_DETAIL) + if (color != 0) { + binding.completionDetail.setTextColor(color) + } + + color = getThemeColor(COMPLETION_WND_TEXT_API) + if (color != 0) { + binding.completionApiInfo.setTextColor(color) + } + + color = getThemeColor(COMPLETION_WND_TEXT_TYPE) + if (color != 0) { + binding.completionType.setTextColor(color) + } + } + + private fun setItemBackground(binding: LayoutCompletionItemBinding, isCurrent: Boolean) { + val color = + if (isCurrent) getThemeColor(SchemeAndroidIDE.COMPLETION_WND_BG_CURRENT_ITEM) + else 0 + + val cornerRadius = binding.root.context.resources + .getDimensionPixelSize(R.dimen.completion_window_corner_radius).toFloat() + + val gd = GradientDrawable().apply { + setColor(color) + setCornerRadius(cornerRadius) + } + + binding.root.background = gd + } + + private fun showApiInfoIfNeeded(item: LspCompletionItem, textView: TextView) { + executeAsync({ + if (!isValidForApiVersion(item)) { + return@executeAsync null + } + + val data = item.data + val versions = + Lookup.getDefault().lookup(ApiVersions.COMPLETION_LOOKUP_KEY) + ?: return@executeAsync null + val className = + when (data) { + is ClassCompletionData -> data.className + is MemberCompletionData -> data.classInfo.className + else -> return@executeAsync null + } + val kind = item.completionKind + + val clazz = versions.getClass(className) ?: return@executeAsync null + var info: Info? = clazz + + if (data is MethodCompletionData) { + if ( + kind == METHOD && data.erasedParameterTypes.isNotEmpty() && data.memberName.isNotBlank() + ) { + val method = + clazz.getMethod(data.memberName, *data.erasedParameterTypes.toTypedArray()) + if (method != null) { + info = method + } + } else if (kind == FIELD && data.memberName.isNotBlank()) { + val field = clazz.getField(data.memberName) + if (field != null) { + info = field + } + } + } + val sb = StringBuilder() + if (info!!.since > 1) { + sb.append(textView.context.getString(msg_api_info_since, info.since)) + sb.append("\n") + } + + if (info.removed > 0) { + sb.append(textView.context.getString(msg_api_info_removed, info.removed)) + sb.append("\n") + } + + if (info.deprecated > 0) { + sb.append(textView.context.getString(msg_api_info_deprecated, info.deprecated)) + sb.append("\n") + } + + return@executeAsync sb + }) { + if (it.isNullOrBlank()) { + textView.visibility = View.GONE + return@executeAsync + } + + textView.text = it + textView.visibility = View.VISIBLE + } + } + + private fun isValidForApiVersion(item: LspCompletionItem?): Boolean { + if (item == null) { + return false + } + val type = item.completionKind + val data = item.data + return if ( // These represent a class type + (type === CLASS || + type === INTERFACE || + type === ENUM || + + // These represent a method type + type === METHOD || + type === CONSTRUCTOR || + + // A field type + type === FIELD) && data != null + ) { + val className = + when (data) { + is ClassCompletionData -> data.className + is MemberCompletionData -> data.classInfo.className + else -> null + } + !TextUtils.isEmpty(className) + } else false + } } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/CommonCompletionProvider.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/CommonCompletionProvider.kt index d78c153867..01fc98b186 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/CommonCompletionProvider.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/CommonCompletionProvider.kt @@ -39,59 +39,61 @@ import java.util.concurrent.CancellationException * @author Akash Yadav */ internal class CommonCompletionProvider( - private val server: ILanguageServer, - private val cancelChecker: CompletionCancelChecker + private val server: ILanguageServer, + private val cancelChecker: CompletionCancelChecker ) { - companion object { + companion object { - private val log = LoggerFactory.getLogger(CommonCompletionProvider::class.java) - } + private val log = LoggerFactory.getLogger(CommonCompletionProvider::class.java) + } - /** - * Computes completion items using the provided language server instance. - * - * @param content The reference to the content of the editor. - * @param file The file to compute completions for. - * @param position The position of the cursor in the content. - * @return The computed completion items. May return an empty list if the there was an error - * computing the completion items. - */ - inline fun complete( - content: ContentReference, - file: Path, - position: CharPosition, - prefixMatcher: (Char) -> Boolean - ): List { - val completionResult = - try { - setupLookupForCompletion(file) - val prefix = CompletionHelper.computePrefix(content, position, prefixMatcher) - val params = - CompletionParams(Position(position.line, position.column, position.index), file, - cancelChecker) - params.content = content - params.prefix = prefix - server.complete(params) - } catch (e: Throwable) { + /** + * Computes completion items using the provided language server instance. + * + * @param content The reference to the content of the editor. + * @param file The file to compute completions for. + * @param position The position of the cursor in the content. + * @return The computed completion items. May return an empty list if the there was an error + * computing the completion items. + */ + inline fun complete( + content: ContentReference, + file: Path, + position: CharPosition, + prefixMatcher: (Char) -> Boolean + ): List { + val completionResult = + try { + setupLookupForCompletion(file) + val prefix = CompletionHelper.computePrefix(content, position, prefixMatcher) + val params = + CompletionParams( + Position(position.line, position.column, position.index), file, + cancelChecker + ) + params.content = content + params.prefix = prefix + server.complete(params) + } catch (e: Throwable) { - if (e is CancellationException) { - log.debug("Completion process cancelled") - } + if (e is CancellationException) { + log.debug("Completion process cancelled") + } - // Do not log if completion was interrupted or cancelled - if (!(e is CancellationException || e is CompletionCancelledException)) { - if (!server.handleFailure(LSPFailure(COMPLETION, e))) { - log.error("Unable to compute completions", e) - } - } - CompletionResult.EMPTY - } + // Do not log if completion was interrupted or cancelled + if (!(e is CancellationException || e is CompletionCancelledException)) { + if (!server.handleFailure(LSPFailure(COMPLETION, e))) { + log.error("Unable to compute completions", e) + } + } + CompletionResult.EMPTY + } - if (completionResult == CompletionResult.EMPTY) { - return listOf() - } + if (completionResult == CompletionResult.EMPTY) { + return listOf() + } - return completionResult.items - } + return completionResult.items + } } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt index 653ffc88ad..7b38f93c05 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt @@ -42,95 +42,97 @@ import java.nio.file.Paths */ abstract class IDELanguage : Language { - private var formatter: Formatter? = null - - protected open val languageServer: ILanguageServer? - get() = null - - open fun getTabSize(): Int { - return EditorPreferences.tabSize - } - - open fun addBreakpoint(line: Int) {} - open fun addBreakpoints(lines: Iterable) = lines.forEach(::addBreakpoint) - open fun removeBreakpoint(line: Int) {} - open fun removeBreakpoints(lines: Iterable) = lines.forEach(::removeBreakpoint) - open fun removeAllBreakpoints() {} - open fun toggleBreakpoint(line: Int) {} - open fun highlightLine(line: Int) {} - open fun unhighlightLines() {} - - @Throws(CompletionCancelledException::class) - override fun requireAutoComplete( - content: ContentReference, - position: CharPosition, - publisher: CompletionPublisher, - extraArguments: Bundle - ) { - try { - val cancelChecker = CompletionCancelChecker(publisher) - Lookup.getDefault().register(ICancelChecker::class.java, cancelChecker) - doComplete(content, position, publisher, cancelChecker, extraArguments) - } finally { - Lookup.getDefault().unregister( - ICancelChecker::class.java) - } - } - - private fun doComplete( - content: ContentReference, - position: CharPosition, - publisher: CompletionPublisher, - cancelChecker: CompletionCancelChecker, - extraArguments: Bundle - ) { - val server = languageServer ?: return - val path = extraArguments.getString(IEditor.KEY_FILE, null) - if (path == null) { - log.warn("Cannot provide completions. No file provided.") - return - } - - val completionProvider = CommonCompletionProvider(server, cancelChecker) - val file = Paths.get(path) - val completionItems = completionProvider.complete(content, file, - position) { checkIsCompletionChar(it) } - publisher.setUpdateThreshold(1) - (publisher as IDECompletionPublisher).addLSPItems(completionItems) - } - - /** - * Check if the given character is a completion character. - * - * @param c The character to check. - * @return `true` if the character is completion char, `false` otherwise. - */ - protected open fun checkIsCompletionChar(c: Char): Boolean { - return false - } - - override fun useTab(): Boolean { - return !EditorPreferences.useSoftTab - } - - override fun getFormatter(): Formatter { - return formatter ?: LSPFormatter(languageServer).also { formatter = it } - } - - override fun getIndentAdvance( - content: ContentReference, - line: Int, - column: Int - ): Int { - return getIndentAdvance(content.getLine(line).substring(0, column)) - } - - open fun getIndentAdvance(line: String): Int { - return 0 - } - - companion object { - - private val log = LoggerFactory.getLogger(IDELanguage::class.java) - } + private var formatter: Formatter? = null + + protected open val languageServer: ILanguageServer? + get() = null + + open fun getTabSize(): Int { + return EditorPreferences.tabSize + } + + open fun addBreakpoint(line: Int) {} + open fun addBreakpoints(lines: Iterable) = lines.forEach(::addBreakpoint) + open fun removeBreakpoint(line: Int) {} + open fun removeBreakpoints(lines: Iterable) = lines.forEach(::removeBreakpoint) + open fun removeAllBreakpoints() {} + open fun toggleBreakpoint(line: Int) {} + open fun highlightLine(line: Int) {} + open fun unhighlightLines() {} + + @Throws(CompletionCancelledException::class) + override fun requireAutoComplete( + content: ContentReference, + position: CharPosition, + publisher: CompletionPublisher, + extraArguments: Bundle + ) { + try { + val cancelChecker = CompletionCancelChecker(publisher) + Lookup.getDefault().register(ICancelChecker::class.java, cancelChecker) + doComplete(content, position, publisher, cancelChecker, extraArguments) + } finally { + Lookup.getDefault().unregister( + ICancelChecker::class.java + ) + } + } + + private fun doComplete( + content: ContentReference, + position: CharPosition, + publisher: CompletionPublisher, + cancelChecker: CompletionCancelChecker, + extraArguments: Bundle + ) { + val server = languageServer ?: return + val path = extraArguments.getString(IEditor.KEY_FILE, null) + if (path == null) { + log.warn("Cannot provide completions. No file provided.") + return + } + + val completionProvider = CommonCompletionProvider(server, cancelChecker) + val file = Paths.get(path) + val completionItems = + completionProvider.complete(content, file, position) { checkIsCompletionChar(it) } + + publisher.setUpdateThreshold(1) + (publisher as IDECompletionPublisher).addLSPItems(completionItems) + } + + /** + * Check if the given character is a completion character. + * + * @param c The character to check. + * @return `true` if the character is completion char, `false` otherwise. + */ + protected open fun checkIsCompletionChar(c: Char): Boolean { + return false + } + + override fun useTab(): Boolean { + return !EditorPreferences.useSoftTab + } + + override fun getFormatter(): Formatter { + return formatter ?: LSPFormatter(languageServer).also { formatter = it } + } + + override fun getIndentAdvance( + content: ContentReference, + line: Int, + column: Int + ): Int { + return getIndentAdvance(content.getLine(line).substring(0, column)) + } + + open fun getIndentAdvance(line: String): Int { + return 0 + } + + companion object { + + private val log = LoggerFactory.getLogger(IDELanguage::class.java) + } } \ No newline at end of file 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 bf0e4d9ddd..c7e9223978 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 @@ -124,10 +124,12 @@ class KotlinLanguageServer : ILanguageServer { override fun setupWithProject(workspace: Workspace) { logger.info("setupWithProject called, initialized={}", initialized) - (ProjectManagerImpl.getInstance() + val indexingServiceManager = ProjectManagerImpl.getInstance() .indexingServiceManager - .getService(JvmIndexingService.ID) as? JvmIndexingService?) - ?.refresh() + val jvmIndexingService = + indexingServiceManager.getService(JvmIndexingService.ID) as? JvmIndexingService? + + jvmIndexingService?.refresh() val jdkHome = Environment.JAVA_HOME.toPath() val jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE 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 6bb19535d9..c7a322289d 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 @@ -52,7 +52,7 @@ import kotlin.io.path.pathString * @param jdkRelease The JDK release version at [jdkHome]. */ internal class CompilationEnvironment( - val projectModel: KotlinProjectModel, + val project: KotlinProjectModel, val intellijPluginRoot: Path, val jdkHome: Path, val jdkRelease: Int, @@ -82,11 +82,8 @@ internal class CompilationEnvironment( val coreApplicationEnvironment: CoreApplicationEnvironment get() = session.coreApplicationEnvironment - val moduleResolver: ModuleResolver? - get() = projectModel.moduleResolver - val symbolVisibilityChecker: SymbolVisibilityChecker? - get() = projectModel.symbolVisibilityChecker + get() = project.symbolVisibilityChecker private val envMessageCollector = object : MessageCollector { override fun clear() { @@ -115,7 +112,7 @@ internal class CompilationEnvironment( parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) fileManager = KtFileManager(parser, psiManager, psiDocumentManager) - projectModel.addListener(this) + project.addListener(this) } private fun buildSession(): StandaloneAnalysisAPISession { @@ -127,7 +124,7 @@ internal class CompilationEnvironment( compilerConfiguration = configuration, ) { buildKtModuleProvider { - projectModel.configureModules(this) + this@CompilationEnvironment.project.configureModules(this) } } @@ -239,7 +236,7 @@ internal class CompilationEnvironment( override fun close() { fileManager.close() - projectModel.removeListener(this) + project.removeListener(this) disposable.dispose() } 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 48bde185b6..9d501b0d65 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 @@ -35,7 +35,7 @@ internal class Compiler( init { defaultCompilationEnv = CompilationEnvironment( - projectModel = projectModel, + project = projectModel, intellijPluginRoot = intellijPluginRoot, jdkHome = jdkHome, jdkRelease = jdkRelease, 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 06bd635704..4354826bb1 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,10 +1,13 @@ package com.itsaky.androidide.lsp.kotlin.compiler import com.itsaky.androidide.lsp.kotlin.completion.SymbolVisibilityChecker +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_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 @@ -44,6 +47,12 @@ internal class KotlinProjectModel { val symbolVisibilityChecker: SymbolVisibilityChecker? get() = _symbolVisibilityChecker + val libraryIndex: JvmLibrarySymbolIndex? + get() = ProjectManagerImpl.getInstance() + .indexingServiceManager + .registry + .get(JVM_LIBRARY_SYMBOL_INDEX) + /** * The kind of change that occurred. */ 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 ca003ee1c8..c83fb63749 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 @@ -1,14 +1,26 @@ 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.models.ClassCompletionData import com.itsaky.androidide.lsp.models.Command import com.itsaky.androidide.lsp.models.CompletionItem import com.itsaky.androidide.lsp.models.CompletionItemKind import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.InsertTextFormat +import com.itsaky.androidide.lsp.models.MethodCompletionData +import com.itsaky.androidide.lsp.models.SnippetDescription import com.itsaky.androidide.projects.FileManager +import com.itsaky.androidide.projects.ProjectManagerImpl import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.runBlocking +import org.appdevforall.codeonthego.indexing.jvm.JVM_LIBRARY_SYMBOL_INDEX +import org.appdevforall.codeonthego.indexing.jvm.JvmClassInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmFunctionInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolKind +import org.appdevforall.codeonthego.indexing.jvm.JvmTypeAliasInfo import org.jetbrains.kotlin.analysis.api.KaContextParameterApi import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.KaSession @@ -44,6 +56,7 @@ import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.types.Variance import org.slf4j.LoggerFactory +import kotlin.math.log private const val KT_COMPLETION_PLACEHOLDER = "KT_COMPLETION_PLACEHOLDER" @@ -118,14 +131,16 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi when (completionContext) { CompletionContext.Scope -> - collectScopeCompletions( - scopeContext = scopeContext, - scope = compositeScope, - symbolVisibilityChecker = symbolVisibilityChecker, - ktElement = ktElement, - partial = partial, - to = items - ) + runBlocking { + collectScopeCompletions( + scopeContext = scopeContext, + scope = compositeScope, + symbolVisibilityChecker = symbolVisibilityChecker, + ktElement = ktElement, + partial = partial, + to = items + ) + } CompletionContext.Member -> collectMemberCompletions( @@ -242,7 +257,7 @@ private fun KaSession.collectExtensionFunctions( to += toCompletionItems(extensionSymbols, partial) } -private fun KaSession.collectScopeCompletions( +private suspend fun KaSession.collectScopeCompletions( scopeContext: KaScopeContext, scope: KaScope, symbolVisibilityChecker: SymbolVisibilityChecker, @@ -274,6 +289,76 @@ private fun KaSession.collectScopeCompletions( to += toCompletionItems(callables, partial) to += toCompletionItems(classifiers, partial) + + val librarySymbolIndex = ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(JVM_LIBRARY_SYMBOL_INDEX) + + if (librarySymbolIndex == null) { + logger.warn("Unable to find JVM library symbol index") + return + } + + val useSiteModule = this.useSiteModule + librarySymbolIndex.findByPrefix(partial) + .collect { symbol -> + val isVisible = symbolVisibilityChecker.isVisible( + symbol = symbol, + useSiteModule = useSiteModule, + useSitePackage = ktElement.containingKtFile.packageDirective?.name + ) + + if (!isVisible) return@collect + + if (symbol.kind.isCallable && !symbol.isTopLevel && !symbol.isExtension) { + // member-level, non-imported callable symbols should not be + // completed in scope completions + return@collect + } + + // TODO: filter-out callables with a receiver type whose receiver + // is not an implicit receiver at the current use-site + + val item = ktCompletionItem( + name = symbol.shortName, + kind = kindOf(symbol), + partial = partial, + ) + + item.overrideTypeText = symbol.returnTypeDisplay + when (symbol.kind) { + JvmSymbolKind.FUNCTION, JvmSymbolKind.CONSTRUCTOR -> { + val data = symbol.data as JvmFunctionInfo + item.detail = data.signatureDisplay + item.setInsertTextForFunction(symbol.shortName, data.parameterCount > 0, partial) + + if (symbol.kind == JvmSymbolKind.CONSTRUCTOR) { + item.overrideTypeText = symbol.shortName + } + } + + 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.data = ClassCompletionData( + className = symbol.fqName, + isNested = classInfo.isInner, + topLevelClass = classInfo.containingClassFqName, + ) + } + + else -> {} + } + + logger.debug("Adding completion item: {}", item) + to += item + } } private fun KaSession.collectKeywordCompletions( @@ -335,16 +420,7 @@ private fun KaSession.callableSymbolToCompletionItem( val hasParams = symbol.valueParameters.isNotEmpty() item.detail = "${name}($params)" - item.insertTextFormat = InsertTextFormat.SNIPPET - item.insertText = if (hasParams) { - "${name}($0)" - } else { - "${name}()$0" - } - - if (hasParams) { - item.command = Command("Trigger parameter hints", Command.TRIGGER_PARAMETER_HINTS) - } + item.setInsertTextForFunction(name, hasParams, partial) // TODO(itsaky): provide method completion data in order to show API info // in completion items @@ -360,6 +436,25 @@ private fun KaSession.callableSymbolToCompletionItem( return item } +private fun CompletionItem.setInsertTextForFunction( + name: String, + hasParams: Boolean, + partial: String, +) { + insertTextFormat = InsertTextFormat.SNIPPET + insertText = if (hasParams) { + "${name}($0)" + } else { + "${name}()$0" + } + + snippetDescription = describeSnippet(prefix = partial, allowCommandExecution = true) + + if (hasParams) { + command = Command("Trigger parameter hints", Command.TRIGGER_PARAMETER_HINTS) + } +} + @OptIn(KaExperimentalApi::class) private fun KaSession.classifierSymbolToCompletionItem( symbol: KaClassifierSymbol, @@ -429,6 +524,28 @@ private fun KaSession.kindOf(symbol: KaSymbol): CompletionItemKind { } } +private fun KaSession.kindOf(symbol: JvmSymbol): CompletionItemKind = + when (symbol.kind) { + JvmSymbolKind.CLASS -> CompletionItemKind.CLASS + JvmSymbolKind.INTERFACE -> CompletionItemKind.INTERFACE + JvmSymbolKind.ENUM -> CompletionItemKind.ENUM + JvmSymbolKind.ENUM_ENTRY -> CompletionItemKind.ENUM_MEMBER + JvmSymbolKind.ANNOTATION_CLASS -> CompletionItemKind.ANNOTATION_TYPE + JvmSymbolKind.OBJECT -> CompletionItemKind.CLASS + JvmSymbolKind.COMPANION_OBJECT -> CompletionItemKind.CLASS + JvmSymbolKind.DATA_CLASS -> CompletionItemKind.CLASS + JvmSymbolKind.VALUE_CLASS -> CompletionItemKind.CLASS + JvmSymbolKind.SEALED_CLASS -> CompletionItemKind.CLASS + JvmSymbolKind.SEALED_INTERFACE -> CompletionItemKind.INTERFACE + JvmSymbolKind.FUNCTION -> CompletionItemKind.FUNCTION + JvmSymbolKind.EXTENSION_FUNCTION -> CompletionItemKind.FUNCTION + JvmSymbolKind.CONSTRUCTOR -> CompletionItemKind.CONSTRUCTOR + JvmSymbolKind.PROPERTY -> CompletionItemKind.PROPERTY + JvmSymbolKind.EXTENSION_PROPERTY -> CompletionItemKind.PROPERTY + JvmSymbolKind.FIELD -> CompletionItemKind.FIELD + JvmSymbolKind.TYPE_ALIAS -> CompletionItemKind.CLASS + } + @OptIn(KaExperimentalApi::class, KaContextParameterApi::class) private fun KaSession.renderName( type: KaType, @@ -445,12 +562,6 @@ private fun partialIdentifier(prefix: String): String { } private fun matchesPrefix(name: Name, partial: String): Boolean { - logger.info( - "'{}' matches '{}': {}", - name, - partial, - name.asString().startsWith(partial, ignoreCase = true) - ) if (partial.isEmpty()) return true return name.asString().startsWith(partial, ignoreCase = true) } diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt index 9665847f60..fb49916024 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt @@ -35,105 +35,114 @@ import org.slf4j.LoggerFactory */ open class DefaultEditHandler : IEditHandler { - companion object { - - private val log = LoggerFactory.getLogger(DefaultEditHandler::class.java) - } - - override fun performEdits( - item: CompletionItem, - editor: CodeEditor, - text: Content, - line: Int, - column: Int, - index: Int - ) { - if (Looper.myLooper() != Looper.getMainLooper()) { - ThreadUtils.runOnUiThread { performEditsInternal(item, editor, text, line, column, index) } - return - } - - performEditsInternal(item, editor, text, line, column, index) - } - - protected open fun performEditsInternal( - item: CompletionItem, - editor: CodeEditor, - text: Content, - line: Int, - column: Int, - index: Int - ) { - if (item.insertTextFormat == SNIPPET) { - insertSnippet(item, editor, text, line, column, index) - return - } - - val start = getIdentifierStart(text.getLine(line), column) - text.delete(line, start, line, column) - editor.commitText(item.insertText) - - text.beginBatchEdit() - if (item.additionalEditHandler != null) { - item.additionalEditHandler!!.performEdits(item, editor, text, line, column, index) - } else if (item.additionalTextEdits != null && item.additionalTextEdits!!.isNotEmpty()) { - RewriteHelper.performEdits(item.additionalTextEdits!!, editor) - } - text.beginBatchEdit() - - executeCommand(editor, item.command) - } - - protected open fun insertSnippet( - item: CompletionItem, - editor: CodeEditor, - text: Content, - line: Int, - column: Int, - index: Int - ) { - val snippetDescription = item.snippetDescription!! - val snippet = CodeSnippetParser.parse(item.insertText) - val prefixLength = snippetDescription.selectedLength - val selectedText = text.subSequence(index - prefixLength, index).toString() - var actionIndex = index - if (snippetDescription.deleteSelected) { - text.delete(index - prefixLength, index) - actionIndex -= prefixLength - } - editor.snippetController.startSnippet(actionIndex, snippet, selectedText) - - if (snippetDescription.allowCommandExecution) { - executeCommand(editor, item.command) - } - } - - protected open fun executeCommand(editor: CodeEditor, command: Command?) { - if (command == null) { - return - } - - try { - val klass = editor::class.java - val method = klass.getMethod("executeCommand", Command::class.java) - method.isAccessible = true - method.invoke(editor, command) - } catch (th: Throwable) { - log.error("Unable to invoke 'executeCommand(Command) method in IDEEditor.", th) - } - } - - protected open fun getIdentifierStart(text: CharSequence, end: Int): Int { - var start = end - while (start > 0) { - if (isPartialPart(text[start - 1])) { - start-- - continue - } - break - } - return start - } - - protected open fun isPartialPart(c: Char) = Character.isJavaIdentifierPart(c) + companion object { + + private val log = LoggerFactory.getLogger(DefaultEditHandler::class.java) + } + + override fun performEdits( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { + if (Looper.myLooper() != Looper.getMainLooper()) { + ThreadUtils.runOnUiThread { + performEditsInternal( + item, + editor, + text, + line, + column, + index + ) + } + return + } + + performEditsInternal(item, editor, text, line, column, index) + } + + protected open fun performEditsInternal( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { + if (item.insertTextFormat == SNIPPET) { + insertSnippet(item, editor, text, line, column, index) + return + } + + val start = getIdentifierStart(text.getLine(line), column) + text.delete(line, start, line, column) + editor.commitText(item.insertText) + + text.beginBatchEdit() + if (item.additionalEditHandler != null) { + item.additionalEditHandler!!.performEdits(item, editor, text, line, column, index) + } else if (item.additionalTextEdits != null && item.additionalTextEdits!!.isNotEmpty()) { + RewriteHelper.performEdits(item.additionalTextEdits!!, editor) + } + text.beginBatchEdit() + + executeCommand(editor, item.command) + } + + protected open fun insertSnippet( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { + val snippetDescription = item.snippetDescription!! + val snippet = CodeSnippetParser.parse(item.insertText) + val prefixLength = snippetDescription.selectedLength + val selectedText = text.subSequence(index - prefixLength, index).toString() + var actionIndex = index + if (snippetDescription.deleteSelected) { + text.delete(index - prefixLength, index) + actionIndex -= prefixLength + } + editor.snippetController.startSnippet(actionIndex, snippet, selectedText) + + if (snippetDescription.allowCommandExecution) { + executeCommand(editor, item.command) + } + } + + protected open fun executeCommand(editor: CodeEditor, command: Command?) { + if (command == null) { + return + } + + try { + val klass = editor::class.java + val method = klass.getMethod("executeCommand", Command::class.java) + method.isAccessible = true + method.invoke(editor, command) + } catch (th: Throwable) { + log.error("Unable to invoke 'executeCommand(Command) method in IDEEditor.", th) + } + } + + protected open fun getIdentifierStart(text: CharSequence, end: Int): Int { + var start = end + while (start > 0) { + if (isPartialPart(text[start - 1])) { + start-- + continue + } + break + } + return start + } + + protected open fun isPartialPart(c: Char) = Character.isJavaIdentifierPart(c) } From c78d5abd0ef424882e700f0cd53e889e66228e39 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 8 Apr 2026 20:03:43 +0530 Subject: [PATCH 31/58] fix: use Kotlin context receivers feature Signed-off-by: Akash Yadav --- lsp/kotlin/build.gradle.kts | 4 + .../kotlin/compiler/CompilationEnvironment.kt | 10 + .../lsp/kotlin/completion/ContextResolver.kt | 15 +- .../kotlin/completion/KotlinCompletions.kt | 187 +++++++----------- 4 files changed, 98 insertions(+), 118 deletions(-) diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index 54c0fa6206..cf1a60bc68 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -25,6 +25,10 @@ plugins { android { namespace = "${BuildConfig.PACKAGE_NAME}.lsp.kotlin" + + kotlin.compilerOptions { + freeCompilerArgs.add("-Xcontext-receivers") + } } kapt { 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 c7a322289d..88521661c9 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 @@ -2,6 +2,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler import com.itsaky.androidide.lsp.kotlin.KtFileManager import com.itsaky.androidide.lsp.kotlin.completion.SymbolVisibilityChecker +import org.appdevforall.codeonthego.indexing.jvm.JvmLibrarySymbolIndex import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory @@ -85,6 +86,15 @@ internal class CompilationEnvironment( val symbolVisibilityChecker: SymbolVisibilityChecker? get() = project.symbolVisibilityChecker + val requireSymbolVisibilityChecker: SymbolVisibilityChecker + get() = checkNotNull(symbolVisibilityChecker) + + val libraryIndex: JvmLibrarySymbolIndex? + get() = project.libraryIndex + + val requireLibraryIndex: JvmLibrarySymbolIndex + get() = checkNotNull(libraryIndex) + private val envMessageCollector = object : MessageCollector { override fun clear() { } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt index 3878cb57ac..2830cc4499 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt @@ -35,13 +35,19 @@ data class CursorContext( val ktFile: KtFile, val ktElement: KtElement, val scopeContext: KaScopeContext, - val compositeScope: KaScope, + val scope: KaScope, val completionContext: CompletionContext, val declarationContext: DeclarationContext, val declarationKind: DeclarationKind, val existingModifiers: Set, val isInsideModifierList: Boolean, -) + val partial: String, +) { + private val importFqns: List by lazy { + ktFile.importDirectives + .mapNotNull { it.importedFqName?.asString() } + } +} private val logger = LoggerFactory.getLogger("ContextResolver") @@ -49,7 +55,7 @@ private val logger = LoggerFactory.getLogger("ContextResolver") /** * Resolves [CursorContext] at the given offset in the given [KtFile]. */ -fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int): CursorContext? { +fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int, partial: String): CursorContext? { val psiElement = ktFile.findElementAt(offset) if (psiElement == null) { logger.error("Unable to find PSI element at offset {} in file {}", offset, ktFile) @@ -83,12 +89,13 @@ fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int): CursorContext? ktFile = ktFile, ktElement = ktElement, scopeContext = scopeContext, - compositeScope = compositeScope, + scope = compositeScope, completionContext = completionContext, declarationContext = declarationContext, declarationKind = declarationKind, existingModifiers = existingModifiers, isInsideModifierList = modifierList != null, + partial = partial, ) } 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 c83fb63749..5f6af4f451 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 @@ -9,13 +9,9 @@ import com.itsaky.androidide.lsp.models.CompletionItemKind import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.InsertTextFormat -import com.itsaky.androidide.lsp.models.MethodCompletionData -import com.itsaky.androidide.lsp.models.SnippetDescription import com.itsaky.androidide.projects.FileManager -import com.itsaky.androidide.projects.ProjectManagerImpl import kotlinx.coroutines.CancellationException import kotlinx.coroutines.runBlocking -import org.appdevforall.codeonthego.indexing.jvm.JVM_LIBRARY_SYMBOL_INDEX import org.appdevforall.codeonthego.indexing.jvm.JvmClassInfo import org.appdevforall.codeonthego.indexing.jvm.JvmFunctionInfo import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol @@ -25,11 +21,9 @@ import org.jetbrains.kotlin.analysis.api.KaContextParameterApi import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.KaSession import org.jetbrains.kotlin.analysis.api.analyzeCopy -import org.jetbrains.kotlin.analysis.api.components.KaScopeContext import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode import org.jetbrains.kotlin.analysis.api.renderer.types.KaTypeRenderer import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource -import org.jetbrains.kotlin.analysis.api.scopes.KaScope import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol @@ -48,15 +42,12 @@ 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.com.intellij.psi.PsiElement import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.types.Variance import org.slf4j.LoggerFactory -import kotlin.math.log private const val KT_COMPLETION_PLACEHOLDER = "KT_COMPLETION_PLACEHOLDER" @@ -102,13 +93,7 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi useSiteElement = completionKtFile, resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, ) { - val symbolVisibilityChecker = this@complete.symbolVisibilityChecker - if (symbolVisibilityChecker == null) { - logger.error("No symbol visibility checker available!") - return@analyzeCopy CompletionResult.EMPTY - } - - val cursorContext = resolveCursorContext(completionKtFile, completionOffset) + val cursorContext = resolveCursorContext(completionKtFile, completionOffset, partial) if (cursorContext == null) { logger.error( "Unable to determine context at offset {} in file {}", @@ -118,46 +103,21 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi return@analyzeCopy CompletionResult.EMPTY } - val ( - psiElement, - _, - ktElement, - scopeContext, - compositeScope, - completionContext - ) = cursorContext - - val items = mutableListOf() - - when (completionContext) { - CompletionContext.Scope -> - runBlocking { - collectScopeCompletions( - scopeContext = scopeContext, - scope = compositeScope, - symbolVisibilityChecker = symbolVisibilityChecker, - ktElement = ktElement, - partial = partial, - to = items - ) + context(cursorContext) { + runBlocking { + val items = mutableListOf() + when (cursorContext.completionContext) { + CompletionContext.Scope -> + collectScopeCompletions(to = items) + + CompletionContext.Member -> + collectMemberCompletions(to = items) } - CompletionContext.Member -> - collectMemberCompletions( - scope = compositeScope, - element = psiElement, - partial = partial, - to = items - ) + collectKeywordCompletions(to = items) + CompletionResult(items) + } } - - collectKeywordCompletions( - ctx = cursorContext, - partial = partial, - to = items - ) - - CompletionResult(items) } } catch (e: Throwable) { if (e is CancellationException) { @@ -169,13 +129,11 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi } } +context(ctx: CursorContext) private fun KaSession.collectMemberCompletions( - scope: KaScope, - element: PsiElement, - partial: String, to: MutableList ) { - val qualifiedExpr = element.getParentOfType(strict = false) + val qualifiedExpr = ctx.psiElement.getParentOfType(strict = false) if (qualifiedExpr == null) { logger.error("No qualified expression found requested position") return @@ -194,36 +152,36 @@ private fun KaSession.collectMemberCompletions( receiver, receiverType, receiver.text, - partial + ctx.partial ) - collectMembersFromType(receiverType, partial, to) + collectMembersFromType(receiverType, to) if (qualifiedExpr is KtSafeQualifiedExpression) { val nonNullType = receiverType.withNullability(isMarkedNullable = false) - collectMembersFromType(nonNullType, partial, to) + collectMembersFromType(nonNullType, to) } - collectExtensionFunctions(scope, partial, receiverType, to) + collectExtensionFunctions(receiverType, to) } +context(ctx: CursorContext) @OptIn(KaExperimentalApi::class) private fun KaSession.collectMembersFromType( receiverType: KaType, - partial: String, to: MutableList ) { val typeScope = receiverType.scope if (typeScope != null) { val callables = - typeScope.getCallableSignatures { name -> matchesPrefix(name, partial) } + typeScope.getCallableSignatures { name -> matchesPrefix(name) } .map { it.symbol } val classifiers = - typeScope.getClassifierSymbols { name -> matchesPrefix(name, partial) } + typeScope.getClassifierSymbols { name -> matchesPrefix(name) } - to += toCompletionItems(callables, partial) - to += toCompletionItems(classifiers, partial) + to += toCompletionItems(callables) + to += toCompletionItems(classifiers) return } @@ -232,21 +190,20 @@ private fun KaSession.collectMembersFromType( val classSymbol = classType.symbol as? KaClassSymbol ?: return val memberScope = classSymbol.memberScope - val callables = memberScope.callables { name -> matchesPrefix(name, partial) } - val classifiers = memberScope.classifiers { name -> matchesPrefix(name, partial) } + val callables = memberScope.callables { name -> matchesPrefix(name) } + val classifiers = memberScope.classifiers { name -> matchesPrefix(name) } - to += toCompletionItems(callables, partial) - to += toCompletionItems(classifiers, partial) + to += toCompletionItems(callables) + to += toCompletionItems(classifiers) } +context(ctx: CursorContext) private fun KaSession.collectExtensionFunctions( - scope: KaScope, - partial: String, receiverType: KaType, to: MutableList ) { val extensionSymbols = - scope.callables { name -> matchesPrefix(name, partial) } + ctx.scope.callables { name -> matchesPrefix(name) } .filter { symbol -> if (!symbol.isExtension) return@filter false @@ -254,26 +211,26 @@ private fun KaSession.collectExtensionFunctions( receiverType.isSubtypeOf(extReceiverType) } - to += toCompletionItems(extensionSymbols, partial) + to += toCompletionItems(extensionSymbols) } +context(env: CompilationEnvironment, ctx: CursorContext) private suspend fun KaSession.collectScopeCompletions( - scopeContext: KaScopeContext, - scope: KaScope, - symbolVisibilityChecker: SymbolVisibilityChecker, - ktElement: KtElement, - partial: String, to: MutableList, ) { + val ktElement = ctx.ktElement + val scope = ctx.scope + val scopeContext = ctx.scopeContext + logger.info( "Complete scope members of {}: [{}] matching '{}'", ktElement, ktElement.text, - partial + ctx.partial ) val callables = - scope.callables { name -> matchesPrefix(name, partial) } + scope.callables { name -> matchesPrefix(name) } .filter { symbol -> // always include non-extension functions @@ -285,26 +242,28 @@ private suspend fun KaSession.collectScopeCompletions( receiver.type.isSubtypeOf(extReceiverType) } } - val classifiers = scope.classifiers { name -> matchesPrefix(name, partial) } - to += toCompletionItems(callables, partial) - to += toCompletionItems(classifiers, partial) + val classifiers = scope.classifiers { name -> matchesPrefix(name) } - val librarySymbolIndex = ProjectManagerImpl - .getInstance() - .indexingServiceManager - .registry - .get(JVM_LIBRARY_SYMBOL_INDEX) + to += toCompletionItems(callables) + to += toCompletionItems(classifiers) + val visibilityChecker = env.symbolVisibilityChecker + if (visibilityChecker == null) { + logger.warn("No visibility checker found") + return + } + + val librarySymbolIndex = env.libraryIndex if (librarySymbolIndex == null) { logger.warn("Unable to find JVM library symbol index") return } val useSiteModule = this.useSiteModule - librarySymbolIndex.findByPrefix(partial) + librarySymbolIndex.findByPrefix(ctx.partial) .collect { symbol -> - val isVisible = symbolVisibilityChecker.isVisible( + val isVisible = visibilityChecker.isVisible( symbol = symbol, useSiteModule = useSiteModule, useSitePackage = ktElement.containingKtFile.packageDirective?.name @@ -324,7 +283,6 @@ private suspend fun KaSession.collectScopeCompletions( val item = ktCompletionItem( name = symbol.shortName, kind = kindOf(symbol), - partial = partial, ) item.overrideTypeText = symbol.returnTypeDisplay @@ -332,7 +290,10 @@ private suspend fun KaSession.collectScopeCompletions( JvmSymbolKind.FUNCTION, JvmSymbolKind.CONSTRUCTOR -> { val data = symbol.data as JvmFunctionInfo item.detail = data.signatureDisplay - item.setInsertTextForFunction(symbol.shortName, data.parameterCount > 0, partial) + item.setInsertTextForFunction( + name = symbol.shortName, + hasParams = data.parameterCount > 0, + ) if (symbol.kind == JvmSymbolKind.CONSTRUCTOR) { item.overrideTypeText = symbol.shortName @@ -361,16 +322,14 @@ private suspend fun KaSession.collectScopeCompletions( } } +context(ctx: CursorContext) private fun KaSession.collectKeywordCompletions( - ctx: CursorContext, - partial: String, to: MutableList, ) { fun kwItem(name: String) = ktCompletionItem( name = name, kind = CompletionItemKind.KEYWORD, - partial = partial ) if (!ctx.isInsideModifierList) { @@ -384,30 +343,30 @@ private fun KaSession.collectKeywordCompletions( } } +context(ctx: CursorContext) @JvmName("callablesToCompletionItems") private fun KaSession.toCompletionItems( callables: Sequence, - partial: String ): Sequence = callables.mapNotNull { - callableSymbolToCompletionItem(it, partial) + callableSymbolToCompletionItem(it) } +context(ctx: CursorContext) @JvmName("classifiersToCompletionItems") private fun KaSession.toCompletionItems( classifiers: Sequence, - partial: String ): Sequence = classifiers.mapNotNull { - classifierSymbolToCompletionItem(it, partial) + classifierSymbolToCompletionItem(it) } +context(ctx: CursorContext) @OptIn(KaExperimentalApi::class) private fun KaSession.callableSymbolToCompletionItem( symbol: KaCallableSymbol, - partial: String ): CompletionItem? { - val item = createSymbolCompletionItem(symbol, partial) ?: return null + val item = createSymbolCompletionItem(symbol) ?: return null val name = item.ideLabel item.overrideTypeText = renderName(symbol.returnType) @@ -420,7 +379,7 @@ private fun KaSession.callableSymbolToCompletionItem( val hasParams = symbol.valueParameters.isNotEmpty() item.detail = "${name}($params)" - item.setInsertTextForFunction(name, hasParams, partial) + item.setInsertTextForFunction(name, hasParams) // TODO(itsaky): provide method completion data in order to show API info // in completion items @@ -436,10 +395,10 @@ private fun KaSession.callableSymbolToCompletionItem( return item } +context(ctx: CursorContext) private fun CompletionItem.setInsertTextForFunction( name: String, hasParams: Boolean, - partial: String, ) { insertTextFormat = InsertTextFormat.SNIPPET insertText = if (hasParams) { @@ -448,19 +407,19 @@ private fun CompletionItem.setInsertTextForFunction( "${name}()$0" } - snippetDescription = describeSnippet(prefix = partial, allowCommandExecution = true) + snippetDescription = describeSnippet(prefix = ctx.partial, allowCommandExecution = true) if (hasParams) { command = Command("Trigger parameter hints", Command.TRIGGER_PARAMETER_HINTS) } } +context(ctx: CursorContext) @OptIn(KaExperimentalApi::class) private fun KaSession.classifierSymbolToCompletionItem( symbol: KaClassifierSymbol, - partial: String ): CompletionItem? { - val item = createSymbolCompletionItem(symbol, partial) ?: return null + val item = createSymbolCompletionItem(symbol) ?: return null item.detail = when (symbol) { is KaClassSymbol -> symbol.classId?.asFqNameString() ?: "" is KaTypeAliasSymbol -> renderName( @@ -473,26 +432,25 @@ private fun KaSession.classifierSymbolToCompletionItem( return item } +context(ctx: CursorContext) private fun KaSession.createSymbolCompletionItem( symbol: KaSymbol, - partial: String ): CompletionItem? { return ktCompletionItem( name = symbol.name?.asString() ?: return null, kind = kindOf(symbol), - partial = partial, ) } +context(ctx: CursorContext) private fun KaSession.ktCompletionItem( name: String, kind: CompletionItemKind, - partial: String, ): CompletionItem { val item = KotlinCompletionItem() item.ideLabel = name item.completionKind = kind - item.matchLevel = CompletionItem.matchLevel(item.ideLabel, partial) + item.matchLevel = CompletionItem.matchLevel(item.ideLabel, ctx.partial) return item } @@ -561,7 +519,8 @@ private fun partialIdentifier(prefix: String): String { return prefix.takeLastWhile { char -> Character.isJavaIdentifierPart(char) } } -private fun matchesPrefix(name: Name, partial: String): Boolean { - if (partial.isEmpty()) return true - return name.asString().startsWith(partial, ignoreCase = true) +context(ctx: CursorContext) +private fun matchesPrefix(name: Name): Boolean { + if (ctx.partial.isEmpty()) return true + return name.asString().startsWith(ctx.partial, ignoreCase = true) } From 47d1738513aa12513ccba67dfc263ede63889528 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 9 Apr 2026 12:25:27 +0530 Subject: [PATCH 32/58] fix: auto-import un-imported classes Signed-off-by: Akash Yadav --- .../lsp/java/edits/AdvancedJavaEditHandler.kt | 53 ++++---- .../lsp/java/edits/ClassImportEditHandler.kt | 24 ++-- lsp/kotlin/build.gradle.kts | 2 +- .../androidide/lsp/kotlin/KtFileManager.kt | 19 ++- .../kotlin/compiler/CompilationEnvironment.kt | 2 +- .../lsp/kotlin/compiler/KotlinProjectModel.kt | 2 +- .../completion/AdvancedKotlinEditHandler.kt | 43 +++++++ .../lsp/kotlin/completion/ContextKeywords.kt | 53 -------- .../KotlinClassImportEditHandler.kt | 27 ++++ .../kotlin/completion/KotlinCompletions.kt | 119 +++++++++++++++--- .../diagnostic/KotlinDiagnosticProvider.kt | 20 +-- .../lsp/kotlin/utils/ContextKeywords.kt | 74 +++++++++++ .../{completion => utils}/ContextResolver.kt | 72 ++++------- .../androidide/lsp/kotlin/utils/EditExts.kt | 95 ++++++++++++++ .../{completion => utils}/ModifierFilter.kt | 12 +- .../androidide/lsp/kotlin/utils/SymbolExts.kt | 25 ++++ .../SymbolVisibilityChecker.kt | 2 +- .../androidide/lsp/models/CompletionData.kt | 42 +++---- .../androidide/lsp/util/RewriteHelper.kt | 43 ++++--- 19 files changed, 504 insertions(+), 225 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt rename lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/{completion => utils}/ContextResolver.kt (73%) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt rename lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/{completion => utils}/ModifierFilter.kt (93%) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt rename lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/{completion => utils}/SymbolVisibilityChecker.kt (98%) diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt index 7b8fed0d0d..387d546cb4 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt @@ -32,32 +32,33 @@ import java.nio.file.Path */ abstract class AdvancedJavaEditHandler(protected val file: Path) : BaseJavaEditHandler() { - override fun performEdits( - item: CompletionItem, - editor: CodeEditor, - text: Content, - line: Int, - column: Int, - index: Int - ) { - val compiler = JavaCompilerProvider.get( - IProjectManager.getInstance().findModuleForFile(file, false) ?: return) - performEdits(compiler, editor, item) + override fun performEdits( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { + val compiler = JavaCompilerProvider.get( + IProjectManager.getInstance().findModuleForFile(file, false) ?: return + ) + performEdits(compiler, editor, item) - executeCommand(editor, item.command) - } + executeCommand(editor, item.command) + } - /** - * Java edit handlers which require instance of the compiler should override this method instead - * of [performEdits]. - * - * @param compiler The compiler service instance. - * @param editor The editor to perform edits on. - * @param completionItem The completion item which contains required data. - */ - abstract fun performEdits( - compiler: JavaCompilerService, - editor: CodeEditor, - completionItem: CompletionItem - ) + /** + * Java edit handlers which require instance of the compiler should override this method instead + * of [performEdits]. + * + * @param compiler The compiler service instance. + * @param editor The editor to perform edits on. + * @param completionItem The completion item which contains required data. + */ + abstract fun performEdits( + compiler: JavaCompilerService, + editor: CodeEditor, + completionItem: CompletionItem + ) } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt index 6812e9df78..d78a8e673d 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt @@ -34,17 +34,17 @@ import java.nio.file.Path */ class ClassImportEditHandler(val imports: Set, file: Path) : AdvancedJavaEditHandler(file) { - override fun performEdits( - compiler: JavaCompilerService, - editor: CodeEditor, - completionItem: CompletionItem - ) { - val data = completionItem.data as? ClassCompletionData ?: return - val className = data.className - val edits = EditHelper.addImportIfNeeded(compiler, file, imports, className) + override fun performEdits( + compiler: JavaCompilerService, + editor: CodeEditor, + completionItem: CompletionItem + ) { + val data = completionItem.data as? ClassCompletionData ?: return + val className = data.className + val edits = EditHelper.addImportIfNeeded(compiler, file, imports, className) - if (edits.isNotEmpty()) { - RewriteHelper.performEdits(edits, editor) - } - } + if (edits.isNotEmpty()) { + RewriteHelper.performEdits(edits, editor) + } + } } diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index cf1a60bc68..a4fcabb56b 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -27,7 +27,7 @@ android { namespace = "${BuildConfig.PACKAGE_NAME}.lsp.kotlin" kotlin.compilerOptions { - freeCompilerArgs.add("-Xcontext-receivers") + freeCompilerArgs.addAll("-Xcontext-parameters") } } 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 index 2941cfb2c1..c711534897 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -125,7 +126,6 @@ class KtFileManager( updateDocumentContent(entry, content) logger.debug("File opened and managed: {}", path) - return } override fun onFileContentChanged(path: Path, content: String) { @@ -150,7 +150,22 @@ class KtFileManager( logger.debug("File closed: {}", path) } - fun getOpenFile(path: Path): ManagedFile? = entries[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() 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 88521661c9..714179a548 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,7 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler import com.itsaky.androidide.lsp.kotlin.KtFileManager -import com.itsaky.androidide.lsp.kotlin.completion.SymbolVisibilityChecker +import com.itsaky.androidide.lsp.kotlin.utils.SymbolVisibilityChecker import org.appdevforall.codeonthego.indexing.jvm.JvmLibrarySymbolIndex import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory 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 4354826bb1..12e11306d4 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,6 +1,6 @@ package com.itsaky.androidide.lsp.kotlin.compiler -import com.itsaky.androidide.lsp.kotlin.completion.SymbolVisibilityChecker +import com.itsaky.androidide.lsp.kotlin.utils.SymbolVisibilityChecker import com.itsaky.androidide.projects.ProjectManagerImpl import com.itsaky.androidide.projects.api.AndroidModule import com.itsaky.androidide.projects.api.ModuleProject 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 new file mode 100644 index 0000000000..c63a908733 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt @@ -0,0 +1,43 @@ +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.slf4j.LoggerFactory + +internal abstract class AdvancedKotlinEditHandler( + protected val analysisContext: AnalysisContext, +) : BaseKotlinEditHandler() { + + companion object { + private val logger = LoggerFactory.getLogger(AdvancedKotlinEditHandler::class.java) + } + + override fun performEdits( + item: CompletionItem, + editor: CodeEditor, + text: Content, + line: Int, + column: Int, + index: Int + ) { + val managedFile = analysisContext.env.fileManager.getOpenFile(analysisContext.file) + if (managedFile == null) { + logger.error("Unable to perform edit. File not open.") + return + } + + performEdits(managedFile, editor, item) + if (item.command != null) { + executeCommand(editor, item.command) + } + } + + abstract fun performEdits( + managedFile: KtFileManager.ManagedFile, + editor: CodeEditor, + item: CompletionItem + ) +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt deleted file mode 100644 index 433963f83e..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt +++ /dev/null @@ -1,53 +0,0 @@ -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 = 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 - } -} \ No newline at end of file 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 new file mode 100644 index 0000000000..d58b2950ee --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt @@ -0,0 +1,27 @@ +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 + +internal class KotlinClassImportEditHandler( + analysisContext: AnalysisContext, +) : AdvancedKotlinEditHandler(analysisContext) { + override fun performEdits( + managedFile: KtFileManager.ManagedFile, + editor: CodeEditor, + item: CompletionItem + ) { + val data = item.data as? ClassCompletionData ?: return + context(analysisContext) { + val edits = insertImport(data.className) + if (edits.isNotEmpty()) { + RewriteHelper.performEdits(edits, editor) + } + } + } +} \ No newline at end of file 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 5f6af4f451..e1ca3b905f 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,11 @@ 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.utils.AnalysisContext +import com.itsaky.androidide.lsp.kotlin.utils.ContextKeywords +import com.itsaky.androidide.lsp.kotlin.utils.ModifierFilter +import com.itsaky.androidide.lsp.kotlin.utils.containingTopLevelClassDeclaration +import com.itsaky.androidide.lsp.kotlin.utils.resolveAnalysisContext import com.itsaky.androidide.lsp.models.ClassCompletionData import com.itsaky.androidide.lsp.models.Command import com.itsaky.androidide.lsp.models.CompletionItem @@ -19,6 +24,7 @@ import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolKind import org.appdevforall.codeonthego.indexing.jvm.JvmTypeAliasInfo import org.jetbrains.kotlin.analysis.api.KaContextParameterApi import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.KaIdeApi import org.jetbrains.kotlin.analysis.api.KaSession import org.jetbrains.kotlin.analysis.api.analyzeCopy import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode @@ -26,6 +32,7 @@ import org.jetbrains.kotlin.analysis.api.renderer.types.KaTypeRenderer import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind +import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaClassifierSymbol import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol @@ -42,10 +49,13 @@ 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.com.intellij.psi.PsiElement import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType +import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.types.Variance import org.slf4j.LoggerFactory @@ -93,8 +103,16 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi useSiteElement = completionKtFile, resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, ) { - val cursorContext = resolveCursorContext(completionKtFile, completionOffset, partial) - if (cursorContext == null) { + 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, @@ -103,10 +121,11 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi return@analyzeCopy CompletionResult.EMPTY } - context(cursorContext) { + context(ctx) { runBlocking { val items = mutableListOf() - when (cursorContext.completionContext) { + val completionContext = determineCompletionContext(ctx.psiElement) + when (completionContext) { CompletionContext.Scope -> collectScopeCompletions(to = items) @@ -129,7 +148,7 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi } } -context(ctx: CursorContext) +context(ctx: AnalysisContext) private fun KaSession.collectMemberCompletions( to: MutableList ) { @@ -165,7 +184,7 @@ private fun KaSession.collectMemberCompletions( collectExtensionFunctions(receiverType, to) } -context(ctx: CursorContext) +context(ctx: AnalysisContext) @OptIn(KaExperimentalApi::class) private fun KaSession.collectMembersFromType( receiverType: KaType, @@ -197,7 +216,7 @@ private fun KaSession.collectMembersFromType( to += toCompletionItems(classifiers) } -context(ctx: CursorContext) +context(ctx: AnalysisContext) private fun KaSession.collectExtensionFunctions( receiverType: KaType, to: MutableList @@ -214,7 +233,7 @@ private fun KaSession.collectExtensionFunctions( to += toCompletionItems(extensionSymbols) } -context(env: CompilationEnvironment, ctx: CursorContext) +context(env: CompilationEnvironment, ctx: AnalysisContext) private suspend fun KaSession.collectScopeCompletions( to: MutableList, ) { @@ -248,6 +267,13 @@ private suspend fun KaSession.collectScopeCompletions( to += toCompletionItems(callables) to += toCompletionItems(classifiers) + collectUnimportedSymbols(to) +} + +context(env: CompilationEnvironment, ctx: AnalysisContext) +private suspend fun KaSession.collectUnimportedSymbols( + to: MutableList +) { val visibilityChecker = env.symbolVisibilityChecker if (visibilityChecker == null) { logger.warn("No visibility checker found") @@ -266,7 +292,7 @@ private suspend fun KaSession.collectScopeCompletions( val isVisible = visibilityChecker.isVisible( symbol = symbol, useSiteModule = useSiteModule, - useSitePackage = ktElement.containingKtFile.packageDirective?.name + useSitePackage = ctx.ktElement.containingKtFile.packageDirective?.name ) if (!isVisible) return@collect @@ -307,7 +333,7 @@ private suspend fun KaSession.collectScopeCompletions( in JvmSymbolKind.CLASSIFIER_KINDS -> { val classInfo = symbol.data as JvmClassInfo item.detail = symbol.fqName - item.data = ClassCompletionData( + item.setClassCompletionData( className = symbol.fqName, isNested = classInfo.isInner, topLevelClass = classInfo.containingClassFqName, @@ -322,7 +348,7 @@ private suspend fun KaSession.collectScopeCompletions( } } -context(ctx: CursorContext) +context(ctx: AnalysisContext) private fun KaSession.collectKeywordCompletions( to: MutableList, ) { @@ -343,7 +369,7 @@ private fun KaSession.collectKeywordCompletions( } } -context(ctx: CursorContext) +context(ctx: AnalysisContext) @JvmName("callablesToCompletionItems") private fun KaSession.toCompletionItems( callables: Sequence, @@ -352,7 +378,7 @@ private fun KaSession.toCompletionItems( callableSymbolToCompletionItem(it) } -context(ctx: CursorContext) +context(ctx: AnalysisContext) @JvmName("classifiersToCompletionItems") private fun KaSession.toCompletionItems( classifiers: Sequence, @@ -361,7 +387,7 @@ private fun KaSession.toCompletionItems( classifierSymbolToCompletionItem(it) } -context(ctx: CursorContext) +context(ctx: AnalysisContext) @OptIn(KaExperimentalApi::class) private fun KaSession.callableSymbolToCompletionItem( symbol: KaCallableSymbol, @@ -395,7 +421,7 @@ private fun KaSession.callableSymbolToCompletionItem( return item } -context(ctx: CursorContext) +context(ctx: AnalysisContext) private fun CompletionItem.setInsertTextForFunction( name: String, hasParams: Boolean, @@ -414,8 +440,8 @@ private fun CompletionItem.setInsertTextForFunction( } } -context(ctx: CursorContext) -@OptIn(KaExperimentalApi::class) +context(ctx: AnalysisContext) +@OptIn(KaExperimentalApi::class, KaIdeApi::class) private fun KaSession.classifierSymbolToCompletionItem( symbol: KaClassifierSymbol, ): CompletionItem? { @@ -429,10 +455,38 @@ private fun KaSession.classifierSymbolToCompletionItem( is KaTypeParameterSymbol -> item.ideLabel } + + if (symbol is KaClassLikeSymbol) { + val classFqn = symbol.classId?.asFqNameString() + if (classFqn != null) { + item.setClassCompletionData( + className = classFqn, + isNested = symbol.classId?.isNestedClass ?: false, + topLevelClass = symbol.containingTopLevelClassDeclaration?.classId?.asFqNameString() + ?: "" + ) + } + } + return item } -context(ctx: CursorContext) +context(ctx: AnalysisContext) +private fun CompletionItem.setClassCompletionData( + className: String, + isNested: Boolean = false, + topLevelClass: String = "", +) { + data = ClassCompletionData( + className, + isNested, + topLevelClass + ) + + additionalEditHandler = KotlinClassImportEditHandler(analysisContext = ctx) +} + +context(ctx: AnalysisContext) private fun KaSession.createSymbolCompletionItem( symbol: KaSymbol, ): CompletionItem? { @@ -442,7 +496,7 @@ private fun KaSession.createSymbolCompletionItem( ) } -context(ctx: CursorContext) +context(ctx: AnalysisContext) private fun KaSession.ktCompletionItem( name: String, kind: CompletionItemKind, @@ -519,8 +573,33 @@ private fun partialIdentifier(prefix: String): String { return prefix.takeLastWhile { char -> Character.isJavaIdentifierPart(char) } } -context(ctx: CursorContext) +context(ctx: AnalysisContext) private fun matchesPrefix(name: Name): Boolean { if (ctx.partial.isEmpty()) return true return name.asString().startsWith(ctx.partial, ignoreCase = true) } + +private fun determineCompletionContext(element: PsiElement): CompletionContext { + // Walk up to find a qualified expression where we're the selector + val dotExpr = element.getParentOfType(strict = false) + if (dotExpr != null && isInSelectorPosition(element, dotExpr)) { + return CompletionContext.Member + } + + val safeExpr = element.getParentOfType(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 +} + 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 4472c1a652..fce0b8c721 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,6 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.diagnostic import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment +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 @@ -83,22 +84,3 @@ private fun KaSeverity.toDiagnosticSeverity(): DiagnosticSeverity { } } -private fun TextRange.toRange(containingFile: PsiFile): Range { - val doc = PsiDocumentManager.getInstance(containingFile.project).getDocument(containingFile) - ?: return Range.NONE - val startLine = doc.getLineNumber(startOffset) - val startCol = startOffset - doc.getLineStartOffset(startLine) - val endLine = doc.getLineNumber(endOffset) - val endCol = endOffset - doc.getLineStartOffset(endLine) - return Range( - start = Position( - line = startLine, - column = startCol, - index = startOffset, - ), end = Position( - line = endLine, - column = endCol, - index = endOffset, - ) - ) -} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt new file mode 100644 index 0000000000..c446c08c14 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt @@ -0,0 +1,74 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import com.itsaky.androidide.lsp.kotlin.completion.DeclarationContext +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( + KtTokens.IF_KEYWORD, + KtTokens.ELSE_KEYWORD, + KtTokens.WHEN_KEYWORD, + KtTokens.WHILE_KEYWORD, + KtTokens.DO_KEYWORD, + KtTokens.FOR_KEYWORD, + KtTokens.TRY_KEYWORD, + KtTokens.RETURN_KEYWORD, + KtTokens.THROW_KEYWORD, + KtTokens.BREAK_KEYWORD, + KtTokens.CONTEXT_KEYWORD, + KtTokens.VAL_KEYWORD, + KtTokens.VAR_KEYWORD, + KtTokens.FUN_KEYWORD,// local declarations + KtTokens.OBJECT_KEYWORD,// anonymous / local object + KtTokens.CLASS_KEYWORD,// local class (rare but legal) + ) + + /** Declaration starters at top-level / class body */ + val DECLARATION_KEYWORDS = setOf( + KtTokens.VAL_KEYWORD, + KtTokens.VAR_KEYWORD, + KtTokens.FUN_KEYWORD, + KtTokens.CLASS_KEYWORD, + KtTokens.INTERFACE_KEYWORD, + KtTokens.OBJECT_KEYWORD, + KtTokens.TYPE_ALIAS_KEYWORD, + KtTokens.CONSTRUCTOR_KEYWORD, + KtTokens.INIT_KEYWORD, + ) + + val TOP_LEVEL_ONLY = setOf(KtTokens.PACKAGE_KEYWORD, KtTokens.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 = when (ctx) { + DeclarationContext.TOP_LEVEL, + DeclarationContext.SCRIPT_TOP_LEVEL -> TOP_LEVEL_ONLY + DECLARATION_KEYWORDS + + DeclarationContext.CLASS_BODY -> DECLARATION_KEYWORDS + + setOf(KtTokens.INIT_KEYWORD, KtTokens.CONSTRUCTOR_KEYWORD) + + DeclarationContext.INTERFACE_BODY -> setOf( + KtTokens.VAL_KEYWORD, + KtTokens.VAR_KEYWORD, + KtTokens.FUN_KEYWORD, + KtTokens.CLASS_KEYWORD, + KtTokens.INTERFACE_KEYWORD, + KtTokens.OBJECT_KEYWORD, + KtTokens.TYPE_ALIAS_KEYWORD + ) + + DeclarationContext.OBJECT_BODY, + DeclarationContext.ENUM_BODY -> DECLARATION_KEYWORDS - setOf(KtTokens.CONSTRUCTOR_KEYWORD) + + DeclarationContext.ANNOTATION_BODY -> setOf(KtTokens.VAL_KEYWORD) // annotation params only + + DeclarationContext.FUNCTION_BODY -> STATEMENT_KEYWORDS + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt similarity index 73% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt rename to lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt index 2830cc4499..44299eb427 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt @@ -1,5 +1,8 @@ -package com.itsaky.androidide.lsp.kotlin.completion +package com.itsaky.androidide.lsp.kotlin.utils +import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment +import com.itsaky.androidide.lsp.kotlin.completion.DeclarationContext +import com.itsaky.androidide.lsp.kotlin.completion.DeclarationKind import org.jetbrains.kotlin.analysis.api.KaSession import org.jetbrains.kotlin.analysis.api.components.KaScopeContext import org.jetbrains.kotlin.analysis.api.scopes.KaScope @@ -11,58 +14,60 @@ 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 +import java.nio.file.Path + +private val logger = LoggerFactory.getLogger("ContextResolver") /** * Defines context at the cursor position. */ -data class CursorContext( +internal data class AnalysisContext( + val env: CompilationEnvironment, + val file: Path, val psiElement: PsiElement, val ktFile: KtFile, val ktElement: KtElement, val scopeContext: KaScopeContext, val scope: KaScope, - val completionContext: CompletionContext, val declarationContext: DeclarationContext, val declarationKind: DeclarationKind, val existingModifiers: Set, val isInsideModifierList: Boolean, val partial: String, -) { - private val importFqns: List by lazy { - ktFile.importDirectives - .mapNotNull { it.importedFqName?.asString() } - } -} - - -private val logger = LoggerFactory.getLogger("ContextResolver") +) /** - * Resolves [CursorContext] at the given offset in the given [KtFile]. + * Resolves [AnalysisContext] at the given offset in the given [KtFile]. + * + * @param env The compilation environment. + * @param ktFile The Kotlin file. + * @param offset The offset to resolve context at. + * @param partial The partial identifier at the cursor position. */ -fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int, partial: String): CursorContext? { +internal fun KaSession.resolveAnalysisContext( + env: CompilationEnvironment, + file: Path, + ktFile: KtFile, + offset: Int, + partial: String +): AnalysisContext? { 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(strict = false) if (ktElement == null) { logger.error("Cannot find parent of element {}", psiElement) @@ -84,13 +89,14 @@ fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int, partial: String) val declarationKind = resolveDeclarationKind(ktElement) val declarationContext = resolveDeclarationContext(ktElement) - return CursorContext( + return AnalysisContext( + env = env, + file = file, psiElement = psiElement, ktFile = ktFile, ktElement = ktElement, scopeContext = scopeContext, scope = compositeScope, - completionContext = completionContext, declarationContext = declarationContext, declarationKind = declarationKind, existingModifiers = existingModifiers, @@ -99,30 +105,6 @@ fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int, partial: String) ) } -private fun determineCompletionContext(element: PsiElement): CompletionContext { - // Walk up to find a qualified expression where we're the selector - val dotExpr = element.getParentOfType(strict = false) - if (dotExpr != null && isInSelectorPosition(element, dotExpr)) { - return CompletionContext.Member - } - - val safeExpr = element.getParentOfType(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) { diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt new file mode 100644 index 0000000000..0c395847f4 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt @@ -0,0 +1,95 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import com.itsaky.androidide.lsp.models.TextEdit +import com.itsaky.androidide.models.Position +import com.itsaky.androidide.models.Range +import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.PsiFile +import org.slf4j.LoggerFactory + +private val logger = LoggerFactory.getLogger("EditExts") + +fun TextRange.toRange(containingFile: PsiFile): Range { + val doc = PsiDocumentManager.getInstance(containingFile.project).getDocument(containingFile) + ?: return Range.NONE + val startLine = doc.getLineNumber(startOffset) + val startCol = startOffset - doc.getLineStartOffset(startLine) + val endLine = doc.getLineNumber(endOffset) + val endCol = endOffset - doc.getLineStartOffset(endLine) + return Range( + start = Position( + line = startLine, + column = startCol, + index = startOffset, + ), end = Position( + line = endLine, + column = endCol, + index = endOffset, + ) + ) +} + +context(ctx: AnalysisContext) +internal fun insertImport(fqn: String): List { + val imports = ctx.ktFile.importDirectives + val importText = "import $fqn" + for (import in imports) { + val thisFqn = import.importedFqName?.asString() ?: "" + if (thisFqn == fqn) return emptyList() + if (thisFqn.substringBeforeLast('.') + ".*" == fqn) return emptyList() + + if (fqn < thisFqn) { + logger.info("insert '{}' before '{}'", importText, thisFqn) + return insertBefore(import, importText + System.lineSeparator()) + } + } + + if (imports.isNotEmpty()) { + val last = imports[imports.size - 1] + logger.info("insert {} after last import: {}", importText, last.text) + return insertAfter(last, System.lineSeparator() + importText) + } + + ctx.ktFile.packageDirective?.also { pkg -> + logger.info("insert {} after package stmt: {}", importText, pkg.text) + return insertAfter(pkg, System.lineSeparator() + importText) + } + + logger.info("insert {} at top", importText) + val start = Position(0, 0) + return listOf( + TextEdit( + range = Range(start, start), + newText = importText + System.lineSeparator() + ) + ) +} + +context(ctx: AnalysisContext) +internal fun insertBefore(element: PsiElement, text: String): List { + val range = rangeOf(element) + return listOf( + TextEdit( + range = Range(range.start, range.start), + newText = text + ) + ) +} + +context(ctx: AnalysisContext) +internal fun insertAfter(element: PsiElement, text: String): List { + val range = rangeOf(element) + return listOf( + TextEdit( + range = Range(range.end, range.end), + newText = text + ) + ) +} + +context(ctx: AnalysisContext) +internal fun rangeOf(element: PsiElement): Range { + return element.textRange.toRange(ctx.ktFile) +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt similarity index 93% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt rename to lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt index 5f0710e1d8..406f9f0ac8 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt @@ -1,5 +1,7 @@ -package com.itsaky.androidide.lsp.kotlin.completion +package com.itsaky.androidide.lsp.kotlin.utils +import com.itsaky.androidide.lsp.kotlin.completion.DeclarationContext +import com.itsaky.androidide.lsp.kotlin.completion.DeclarationKind import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtModifierKeywordToken import org.jetbrains.kotlin.lexer.KtTokens.* @@ -7,16 +9,18 @@ import org.jetbrains.kotlin.lexer.KtTokens.* /** * Helper for filtering modifier keywords for keyword completions. */ -object ModifierFilter { +internal object ModifierFilter { /** * Returns which modifier keywords are valid to suggest given the * current context, declaration kind, and already-present modifiers. */ fun validModifiers( - ctx: CursorContext, + ctx: AnalysisContext, ): Set { - val (_, _, _, _, _, _, declCtx, declKind, existing, _) = ctx + val existing = ctx.existingModifiers + val declCtx = ctx.declarationContext + val declKind = ctx.declarationKind val candidates = MODIFIER_KEYWORDS_ARRAY.toMutableSet() candidates -= existing diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt new file mode 100644 index 0000000000..502d5c3aea --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt @@ -0,0 +1,25 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import org.jetbrains.kotlin.analysis.api.KaContextParameterApi +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.components.containingDeclaration +import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol + +@OptIn(KaContextParameterApi::class) +context(session: KaSession) +val KaSymbol.containingTopLevelClassDeclaration: KaClassSymbol? + get() { + var current: KaSymbol? = this + + var lastClass: KaClassSymbol? = null + + while (current != null) { + if (current is KaClassSymbol) { + lastClass = current + } + current = current.containingDeclaration + } + + return lastClass + } \ No newline at end of file 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/utils/SymbolVisibilityChecker.kt similarity index 98% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/SymbolVisibilityChecker.kt rename to lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt index 010b187e41..c8d0398ff6 100644 --- 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/utils/SymbolVisibilityChecker.kt @@ -1,4 +1,4 @@ -package com.itsaky.androidide.lsp.kotlin.completion +package com.itsaky.androidide.lsp.kotlin.utils import com.itsaky.androidide.lsp.kotlin.compiler.ModuleResolver import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/CompletionData.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/CompletionData.kt index a90a310ea3..7ff9a2cb59 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/CompletionData.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/CompletionData.kt @@ -34,19 +34,19 @@ interface ICompletionData data class ClassCompletionData @JvmOverloads constructor(val className: String, val isNested: Boolean = false, val topLevelClass: String = "") : - ICompletionData { - val simpleName: String - get() { - return className.substringAfterLast(delimiter = '.') - } + ICompletionData { + val simpleName: String + get() { + return className.substringAfterLast(delimiter = '.') + } - val nameWithoutTopLevel: String - get() { - if (!isNested) { - return className - } - return className.substring(topLevelClass.length + 1) - } + val nameWithoutTopLevel: String + get() { + if (!isNested) { + return className + } + return className.substring(topLevelClass.length + 1) + } } /** @@ -56,14 +56,14 @@ constructor(val className: String, val isNested: Boolean = false, val topLevelCl * @property classInfo Information about the class [memberName] is a member of. */ interface MemberCompletionData : ICompletionData { - val memberName: String - val classInfo: ClassCompletionData + val memberName: String + val classInfo: ClassCompletionData } /** Information about a field-related completion item. */ data class FieldCompletionData( - override val memberName: String, - override val classInfo: ClassCompletionData + override val memberName: String, + override val classInfo: ClassCompletionData ) : MemberCompletionData /** @@ -73,9 +73,9 @@ data class FieldCompletionData( * @property plusOverloads The number of existing overloaded versions of this method. */ data class MethodCompletionData( - override val memberName: String, - override val classInfo: ClassCompletionData, - val parameterTypes: List, - val erasedParameterTypes: List, - val plusOverloads: Int + override val memberName: String, + override val classInfo: ClassCompletionData, + val parameterTypes: List, + val erasedParameterTypes: List, + val plusOverloads: Int ) : MemberCompletionData diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/util/RewriteHelper.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/util/RewriteHelper.kt index cc117da656..5ff6979c1e 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/util/RewriteHelper.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/util/RewriteHelper.kt @@ -22,24 +22,29 @@ import com.itsaky.androidide.lsp.models.TextEdit import io.github.rosemoe.sora.widget.CodeEditor /** @author Akash Yadav */ -class RewriteHelper { - companion object { - @UiThread - @JvmStatic - fun performEdits(edits: List, editor: CodeEditor) { - if (edits.isEmpty()) { - return - } +object RewriteHelper { + @UiThread + @JvmStatic + fun performEdits(edits: List, editor: CodeEditor) { + if (edits.isEmpty()) { + return + } - edits.forEach { - val s = it.range.start - val e = it.range.end - if (s == e) { - editor.text.insert(s.line, s.column, it.newText) - } else { - editor.text.replace(s.line, s.column, e.line, e.column, it.newText) - } - } - } - } + edits.forEach { + val s = it.range.start + val e = it.range.end + editor.text.apply { + if (s == e) { + val line = s.line + var column = s.column + if (column > getColumnCount(line)) { + column = getColumnCount(line) + } + editor.text.insert(line, column, it.newText) + } else { + editor.text.replace(s.line, s.column, e.line, e.column, it.newText) + } + } + } + } } From eed2d03841c1fb8f992f08d8b003ffeb8961246b Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 9 Apr 2026 15:16:35 +0530 Subject: [PATCH 33/58] fix: use internal name repr for library index Signed-off-by: Akash Yadav --- .../indexing/jvm/JarSymbolScanner.kt | 87 ++++----- .../codeonthego/indexing/jvm/JvmSymbol.kt | 165 +++++++++++------- .../indexing/jvm/JvmSymbolDescriptor.kt | 82 ++++----- .../indexing/jvm/KotlinMetadataScanner.kt | 88 +++++----- .../src/main/proto/jvm_symbol.proto | 36 ++-- .../kotlin/completion/KotlinCompletions.kt | 4 +- 6 files changed, 253 insertions(+), 209 deletions(-) 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 89fd0ab492..19a8422d69 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 @@ -122,20 +122,20 @@ object JarSymbolScanner { val supertypes = buildList { superName?.let { - if (it != "java/lang/Object") add(it.replace('/', '.')) + if (it != "java/lang/Object") add(it) } - interfaces?.forEach { add(it.replace('/', '.')) } + interfaces?.forEach { add(it) } } val containingClass = if (isInnerClass) { - classFqName.split('.').dropLast(1).joinToString(".") + className.substringBeforeLast('$') } else "" symbols.add( JvmSymbol( - key = classFqName, + key = className, sourceId = sourceId, - fqName = classFqName, + name = classFqName, shortName = shortClassName.split('.').last(), packageName = packageName, kind = kind, @@ -143,8 +143,9 @@ object JarSymbolScanner { visibility = visibilityFromAccess(classAccess), isDeprecated = classDeprecated, data = JvmClassInfo( - containingClassFqName = containingClass, - supertypeFqNames = supertypes, + 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), @@ -177,21 +178,21 @@ object JarSymbolScanner { val parameters = paramTypes.map { type -> JvmParameterInfo( name = "", // not available without -parameters flag - typeFqName = typeToFqName(type), - typeDisplay = typeToDisplay(type), + typeName = typeToName(type), + typeDisplayName = typeToDisplayName(type), ) } - val fqName = "$classFqName.$methodName" - val key = "$fqName(${parameters.joinToString(",") { it.typeFqName }})" + val fqName = "$className#$methodName" + val key = "$fqName(${parameters.joinToString(",") { it.typeName }})" val signatureDisplay = buildString { append("(") - append(parameters.joinToString(", ") { it.typeDisplay }) + append(parameters.joinToString(", ") { it.typeDisplayName }) append(")") if (!isConstructor) { append(": ") - append(typeToDisplay(returnType)) + append(typeToDisplayName(returnType)) } } @@ -199,7 +200,7 @@ object JarSymbolScanner { JvmSymbol( key = key, sourceId = sourceId, - fqName = fqName, + name = fqName, shortName = methodName, packageName = packageName, kind = kind, @@ -207,9 +208,9 @@ object JarSymbolScanner { visibility = visibilityFromAccess(access), isDeprecated = classDeprecated, data = JvmFunctionInfo( - containingClassFqName = classFqName, - returnTypeFqName = typeToFqName(returnType), - returnTypeDisplay = typeToDisplay(returnType), + containingClassName = className, + returnTypeName = typeToName(returnType), + returnTypeDisplayName = typeToDisplayName(returnType), parameterCount = paramTypes.size, parameters = parameters, signatureDisplay = signatureDisplay, @@ -234,13 +235,13 @@ object JarSymbolScanner { val fieldType = Type.getType(descriptor) val kind = if (isKotlinClass) JvmSymbolKind.PROPERTY else JvmSymbolKind.FIELD val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA - val fqName = "$classFqName.$name" + val iName = "$className#$name" symbols.add( JvmSymbol( - key = fqName, + key = iName, sourceId = sourceId, - fqName = fqName, + name = iName, shortName = name, packageName = packageName, kind = kind, @@ -248,9 +249,9 @@ object JarSymbolScanner { visibility = visibilityFromAccess(access), isDeprecated = classDeprecated, data = JvmFieldInfo( - containingClassFqName = classFqName, - typeFqName = typeToFqName(fieldType), - typeDisplay = typeToDisplay(fieldType), + containingClassName = className, + typeName = typeToName(fieldType), + typeDisplayName = typeToDisplayName(fieldType), isStatic = hasFlag(access, Opcodes.ACC_STATIC), isFinal = hasFlag(access, Opcodes.ACC_FINAL), constantValue = value?.toString() ?: "", @@ -280,26 +281,34 @@ object JarSymbolScanner { else -> JvmVisibility.PACKAGE_PRIVATE } - private fun typeToFqName(type: Type): String = when (type.sort) { - Type.VOID -> "void" - Type.BOOLEAN -> "boolean" - Type.BYTE -> "byte" - Type.CHAR -> "char" - Type.SHORT -> "short" - Type.INT -> "int" - Type.LONG -> "long" - Type.FLOAT -> "float" - Type.DOUBLE -> "double" - Type.ARRAY -> typeToFqName(type.elementType) + "[]".repeat(type.dimensions) - Type.OBJECT -> type.className - else -> type.className + 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 typeToDisplay(type: Type): String = when (type.sort) { + private fun typeToDisplayName(type: Type): String = when (type.sort) { + Type.BOOLEAN -> "boolean" + Type.BYTE -> "byte" + Type.CHAR -> "char" + Type.SHORT -> "short" + Type.INT -> "int" + Type.LONG -> "long" + Type.FLOAT -> "float" + Type.DOUBLE -> "double" Type.VOID -> "void" - Type.ARRAY -> typeToDisplay(type.elementType) + "[]".repeat(type.dimensions) + Type.ARRAY -> typeToDisplayName(type.elementType) + "[]".repeat(type.dimensions) Type.OBJECT -> type.className.substringAfterLast('.') - else -> typeToFqName(type) + else -> typeToName(type) } } } 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 fdbd2c20bd..81e74380d3 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 @@ -54,40 +54,40 @@ enum class JvmVisibility { * - [JvmTypeAliasInfo] for Kotlin type aliases */ data class JvmSymbol( - override val key: String, - override val sourceId: String, - - val fqName: String, - val shortName: String, - val packageName: String, - val kind: JvmSymbolKind, - val language: JvmSourceLanguage, - val visibility: JvmVisibility = JvmVisibility.PUBLIC, - val isDeprecated: Boolean = false, - - val data: JvmSymbolInfo, + override val key: String, + override val sourceId: String, + + val name: String, + val shortName: String, + val packageName: String, + val kind: JvmSymbolKind, + val language: JvmSourceLanguage, + val visibility: JvmVisibility = JvmVisibility.PUBLIC, + val isDeprecated: Boolean = false, + + val data: JvmSymbolInfo, ) : Indexable { val isTopLevel: Boolean - get() = data.containingClassFqName.isEmpty() + get() = data.containingClassName.isEmpty() val isExtension: Boolean get() = kind.isExtension - val receiverTypeFqName: String? + val receiverTypeName: String? get() = when (val d = data) { - is JvmFunctionInfo -> d.kotlin?.receiverTypeFqName?.takeIf { it.isNotEmpty() } - is JvmFieldInfo -> d.kotlin?.receiverTypeFqName?.takeIf { it.isNotEmpty() } + is JvmFunctionInfo -> d.kotlin?.receiverTypeName?.takeIf { it.isNotEmpty() } + is JvmFieldInfo -> d.kotlin?.receiverTypeName?.takeIf { it.isNotEmpty() } else -> null } - val containingClassFqName: String - get() = data.containingClassFqName + val containingClassName: String + get() = data.containingClassName val returnTypeDisplay: String get() = when (val d = data) { - is JvmFunctionInfo -> d.returnTypeDisplay - is JvmFieldInfo -> d.typeDisplay + is JvmFunctionInfo -> d.returnTypeDisplayName + is JvmFieldInfo -> d.typeDisplayName else -> "" } @@ -100,21 +100,32 @@ data class JvmSymbol( /** * Base for all type-specific symbol data. - * Every variant provides [containingClassFqName] (empty for top-level). + * Every variant provides [containingClassName] (empty for top-level). */ sealed interface JvmSymbolInfo { - val containingClassFqName: String + + /** + * The internal name of the containing class. + */ + val containingClassName: String + + /** + * The fully qualified name of the containing class, in dot format. + */ + val containingClassFqName: String + get() = containingClassName.toFqName() } data class JvmClassInfo( - override val containingClassFqName: String = "", - val supertypeFqNames: List = emptyList(), - val typeParameters: List = emptyList(), - val isAbstract: Boolean = false, - val isFinal: Boolean = false, - val isInner: Boolean = false, - val isStatic: Boolean = false, - val kotlin: KotlinClassInfo? = null, + val internalName: String = "", + override val containingClassName: String = "", + val supertypeNames: List = emptyList(), + val typeParameters: List = emptyList(), + val isAbstract: Boolean = false, + val isFinal: Boolean = false, + val isInner: Boolean = false, + val isStatic: Boolean = false, + val kotlin: KotlinClassInfo? = null, ) : JvmSymbolInfo data class KotlinClassInfo( @@ -130,32 +141,38 @@ data class KotlinClassInfo( ) data class JvmFunctionInfo( - override val containingClassFqName: String = "", - val returnTypeFqName: String = "", - val returnTypeDisplay: String = "", - val parameterCount: Int = 0, - val parameters: List = emptyList(), - val signatureDisplay: String = "", - val typeParameters: List = emptyList(), - val isStatic: Boolean = false, - val isAbstract: Boolean = false, - val isFinal: Boolean = false, - val kotlin: KotlinFunctionInfo? = null, -) : JvmSymbolInfo + override val containingClassName: String = "", + val returnTypeName: String = "", + val returnTypeDisplayName: String = "", + val parameterCount: Int = 0, + val parameters: List = emptyList(), + val signatureDisplay: String = "", + val typeParameters: List = emptyList(), + val isStatic: Boolean = false, + val isAbstract: Boolean = false, + val isFinal: Boolean = false, + val kotlin: KotlinFunctionInfo? = null, +) : JvmSymbolInfo { + val returnTypeFqName: String + get() = returnTypeName.toFqName() +} data class JvmParameterInfo( val name: String, - val typeFqName: String, - val typeDisplay: String, + val typeName: String, + val typeDisplayName: String, val hasDefaultValue: Boolean = false, val isCrossinline: Boolean = false, val isNoinline: Boolean = false, val isVararg: Boolean = false, -) +) { + val typeFqName: String + get() = typeName.toFqName() +} data class KotlinFunctionInfo( - val receiverTypeFqName: String = "", - val receiverTypeDisplay: String = "", + val receiverTypeName: String = "", + val receiverTypeDisplayName: String = "", val isSuspend: Boolean = false, val isInline: Boolean = false, val isInfix: Boolean = false, @@ -165,21 +182,27 @@ data class KotlinFunctionInfo( val isExpect: Boolean = false, val isActual: Boolean = false, val isReturnTypeNullable: Boolean = false, -) +) { + val receiverTypeFqName: String + get() = receiverTypeName.toFqName() +} data class JvmFieldInfo( - override val containingClassFqName: String = "", - val typeFqName: String = "", - val typeDisplay: String = "", - val isStatic: Boolean = false, - val isFinal: Boolean = false, - val constantValue: String = "", - val kotlin: KotlinPropertyInfo? = null, -) : JvmSymbolInfo + override val containingClassName: String = "", + val typeName: String = "", + val typeDisplayName: String = "", + val isStatic: Boolean = false, + val isFinal: Boolean = false, + val constantValue: String = "", + val kotlin: KotlinPropertyInfo? = null, +) : JvmSymbolInfo { + val typeFqName: String + get() = typeName.toFqName() +} data class KotlinPropertyInfo( - val receiverTypeFqName: String = "", - val receiverTypeDisplay: String = "", + val receiverTypeName: String = "", + val receiverTypeDisplayName: String = "", val isConst: Boolean = false, val isLateinit: Boolean = false, val hasGetter: Boolean = false, @@ -189,16 +212,26 @@ data class KotlinPropertyInfo( val isActual: Boolean = false, val isExternal: Boolean = false, val isTypeNullable: Boolean = false, -) +) { + val receiverTypeFqName: String + get() = receiverTypeName.toFqName() +} data class JvmEnumEntryInfo( - override val containingClassFqName: String = "", - val ordinal: Int = 0, + override val containingClassName: String = "", + val ordinal: Int = 0, ) : JvmSymbolInfo data class JvmTypeAliasInfo( - override val containingClassFqName: String = "", - val expandedTypeFqName: String = "", - val expandedTypeDisplay: String = "", - val typeParameters: List = emptyList(), -) : JvmSymbolInfo + override val containingClassName: String = "", + val expandedTypeName: String = "", + val expandedTypeDisplayName: String = "", + val typeParameters: List = emptyList(), +) : JvmSymbolInfo { + val expandedTypeFqName: String + get() = expandedTypeName.toFqName() +} + +private fun String.toFqName() = + replace('/', '.') + .replace('$', '.') \ No newline at end of file diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt index 4d34d1b55d..c414709759 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptor.kt @@ -42,8 +42,8 @@ object JvmSymbolDescriptor : IndexDescriptor { KEY_NAME to entry.shortName, KEY_PACKAGE to entry.packageName, KEY_KIND to entry.kind.name, - KEY_RECEIVER_TYPE to entry.receiverTypeFqName, - KEY_CONTAINING_CLASS to entry.containingClassFqName.ifEmpty { null }, + KEY_RECEIVER_TYPE to entry.receiverTypeName, + KEY_CONTAINING_CLASS to entry.containingClassName.ifEmpty { null }, KEY_LANGUAGE to entry.language.name, ) @@ -55,7 +55,7 @@ object JvmSymbolDescriptor : IndexDescriptor { private fun toProto(s: JvmSymbol): JvmSymbolData { val builder = JvmSymbolData.newBuilder() - .setFqName(s.fqName) + .setName(s.name) .setShortName(s.shortName) .setPackageName(s.packageName) .setSourceId(s.sourceId) @@ -77,8 +77,8 @@ object JvmSymbolDescriptor : IndexDescriptor { private fun classInfoToProto(d: JvmClassInfo): JvmSymbolProtos.ClassData { val builder = JvmSymbolProtos.ClassData.newBuilder() - .setContainingClassFqName(d.containingClassFqName) - .addAllSupertypeFqNames(d.supertypeFqNames) + .setContainingClassName(d.containingClassName) + .addAllSupertypeNames(d.supertypeNames) .addAllTypeParameters(d.typeParameters) .setIsAbstract(d.isAbstract) .setIsFinal(d.isFinal) @@ -105,9 +105,9 @@ object JvmSymbolDescriptor : IndexDescriptor { private fun functionInfoToProto(d: JvmFunctionInfo): JvmSymbolProtos.FunctionData { val builder = JvmSymbolProtos.FunctionData.newBuilder() - .setContainingClassFqName(d.containingClassFqName) - .setReturnTypeFqName(d.returnTypeFqName) - .setReturnTypeDisplay(d.returnTypeDisplay) + .setContainingClassName(d.containingClassName) + .setReturnTypeName(d.returnTypeName) + .setReturnTypeDisplayName(d.returnTypeDisplayName) .setParameterCount(d.parameterCount) .addAllParameters(d.parameters.map { paramToProto(it) }) .setSignatureDisplay(d.signatureDisplay) @@ -119,8 +119,8 @@ object JvmSymbolDescriptor : IndexDescriptor { d.kotlin?.let { kd -> builder.setKotlin( JvmSymbolProtos.KotlinFunctionData.newBuilder() - .setReceiverTypeFqName(kd.receiverTypeFqName) - .setReceiverTypeDisplay(kd.receiverTypeDisplay) + .setReceiverTypeName(kd.receiverTypeName) + .setReceiverTypeDisplayName(kd.receiverTypeDisplayName) .setIsSuspend(kd.isSuspend) .setIsInline(kd.isInline) .setIsInfix(kd.isInfix) @@ -139,8 +139,8 @@ object JvmSymbolDescriptor : IndexDescriptor { private fun paramToProto(p: JvmParameterInfo): JvmSymbolProtos.ParameterData = JvmSymbolProtos.ParameterData.newBuilder() .setName(p.name) - .setTypeFqName(p.typeFqName) - .setTypeDisplay(p.typeDisplay) + .setTypeName(p.typeName) + .setTypeDisplayName(p.typeDisplayName) .setHasDefaultValue(p.hasDefaultValue) .setIsCrossinline(p.isCrossinline) .setIsNoinline(p.isNoinline) @@ -149,9 +149,9 @@ object JvmSymbolDescriptor : IndexDescriptor { private fun fieldInfoToProto(d: JvmFieldInfo): JvmSymbolProtos.FieldData { val builder = JvmSymbolProtos.FieldData.newBuilder() - .setContainingClassFqName(d.containingClassFqName) - .setTypeFqName(d.typeFqName) - .setTypeDisplay(d.typeDisplay) + .setContainingClassName(d.containingClassName) + .setTypeName(d.typeName) + .setTypeDisplayName(d.typeDisplayName) .setIsStatic(d.isStatic) .setIsFinal(d.isFinal) .setConstantValue(d.constantValue) @@ -159,8 +159,8 @@ object JvmSymbolDescriptor : IndexDescriptor { d.kotlin?.let { kd -> builder.setKotlin( JvmSymbolProtos.KotlinPropertyData.newBuilder() - .setReceiverTypeFqName(kd.receiverTypeFqName) - .setReceiverTypeDisplay(kd.receiverTypeDisplay) + .setReceiverTypeName(kd.receiverTypeName) + .setReceiverTypeDisplayName(kd.receiverTypeDisplayName) .setIsConst(kd.isConst) .setIsLateinit(kd.isLateinit) .setHasGetter(kd.hasGetter) @@ -178,14 +178,14 @@ object JvmSymbolDescriptor : IndexDescriptor { private fun enumEntryToProto(d: JvmEnumEntryInfo): JvmSymbolProtos.EnumEntryData = JvmSymbolProtos.EnumEntryData.newBuilder() - .setContainingEnumFqName(d.containingClassFqName) + .setContainingEnumName(d.containingClassName) .setOrdinal(d.ordinal) .build() private fun typeAliasToProto(d: JvmTypeAliasInfo): JvmSymbolProtos.TypeAliasData = JvmSymbolProtos.TypeAliasData.newBuilder() - .setExpandedTypeFqName(d.expandedTypeFqName) - .setExpandedTypeDisplay(d.expandedTypeDisplay) + .setExpandedTypeName(d.expandedTypeName) + .setExpandedTypeDisplayName(d.expandedTypeDisplayName) .addAllTypeParameters(d.typeParameters) .build() @@ -199,17 +199,17 @@ object JvmSymbolDescriptor : IndexDescriptor { && kind != JvmSymbolKind.FIELD -> { val params = (data as? JvmFunctionInfo) ?.parameters - ?.joinToString(",") { it.typeFqName } + ?.joinToString(",") { it.typeName } ?: "" - "${p.fqName}($params)" + "${p.name}($params)" } - else -> p.fqName + else -> p.name } return JvmSymbol( key = key, sourceId = p.sourceId, - fqName = p.fqName, + name = p.name, shortName = p.shortName, packageName = p.packageName, kind = kind, @@ -246,8 +246,8 @@ object JvmSymbolDescriptor : IndexDescriptor { } else null return JvmClassInfo( - containingClassFqName = p.containingClassFqName, - supertypeFqNames = p.supertypeFqNamesList.toList(), + containingClassName = p.containingClassName, + supertypeNames = p.supertypeNamesList.toList(), typeParameters = p.typeParametersList.toList(), isAbstract = p.isAbstract, isFinal = p.isFinal, @@ -261,8 +261,8 @@ object JvmSymbolDescriptor : IndexDescriptor { val kotlin = if (p.hasKotlin()) { val kd = p.kotlin KotlinFunctionInfo( - receiverTypeFqName = kd.receiverTypeFqName, - receiverTypeDisplay = kd.receiverTypeDisplay, + receiverTypeName = kd.receiverTypeName, + receiverTypeDisplayName = kd.receiverTypeDisplayName, isSuspend = kd.isSuspend, isInline = kd.isInline, isInfix = kd.isInfix, @@ -276,9 +276,9 @@ object JvmSymbolDescriptor : IndexDescriptor { } else null return JvmFunctionInfo( - containingClassFqName = p.containingClassFqName, - returnTypeFqName = p.returnTypeFqName, - returnTypeDisplay = p.returnTypeDisplay, + containingClassName = p.containingClassName, + returnTypeName = p.returnTypeName, + returnTypeDisplayName = p.returnTypeDisplayName, parameterCount = p.parameterCount, parameters = p.parametersList.map { paramFromProto(it) }, signatureDisplay = p.signatureDisplay, @@ -293,8 +293,8 @@ object JvmSymbolDescriptor : IndexDescriptor { private fun paramFromProto(p: JvmSymbolProtos.ParameterData): JvmParameterInfo = JvmParameterInfo( name = p.name, - typeFqName = p.typeFqName, - typeDisplay = p.typeDisplay, + typeName = p.typeName, + typeDisplayName = p.typeDisplayName, hasDefaultValue = p.hasDefaultValue, isCrossinline = p.isCrossinline, isNoinline = p.isNoinline, @@ -305,8 +305,8 @@ object JvmSymbolDescriptor : IndexDescriptor { val kotlin = if (p.hasKotlin()) { val kd = p.kotlin KotlinPropertyInfo( - receiverTypeFqName = kd.receiverTypeFqName, - receiverTypeDisplay = kd.receiverTypeDisplay, + receiverTypeName = kd.receiverTypeName, + receiverTypeDisplayName = kd.receiverTypeDisplayName, isConst = kd.isConst, isLateinit = kd.isLateinit, hasGetter = kd.hasGetter, @@ -320,9 +320,9 @@ object JvmSymbolDescriptor : IndexDescriptor { } else null return JvmFieldInfo( - containingClassFqName = p.containingClassFqName, - typeFqName = p.typeFqName, - typeDisplay = p.typeDisplay, + containingClassName = p.containingClassName, + typeName = p.typeName, + typeDisplayName = p.typeDisplayName, isStatic = p.isStatic, isFinal = p.isFinal, constantValue = p.constantValue, @@ -332,14 +332,14 @@ object JvmSymbolDescriptor : IndexDescriptor { private fun enumEntryFromProto(p: JvmSymbolProtos.EnumEntryData): JvmEnumEntryInfo = JvmEnumEntryInfo( - containingClassFqName = p.containingEnumFqName, + containingClassName = p.containingEnumName, ordinal = p.ordinal, ) private fun typeAliasFromProto(p: JvmSymbolProtos.TypeAliasData): JvmTypeAliasInfo = JvmTypeAliasInfo( - expandedTypeFqName = p.expandedTypeFqName, - expandedTypeDisplay = p.expandedTypeDisplay, + expandedTypeName = p.expandedTypeName, + expandedTypeDisplayName = p.expandedTypeDisplayName, typeParameters = p.typeParametersList.toList(), ) 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 691d50dd25..59dc810c8a 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 @@ -112,9 +112,11 @@ object KotlinMetadataScanner { klass: KmClass, sourceId: String, ): List { val symbols = mutableListOf() - val classFqName = klass.name.replace('/', '.') - val packageName = classFqName.substringBeforeLast('.', "") - val shortName = classFqName.substringAfterLast('.') + val className = klass.name + val packageName = className.substringBeforeLast('/') + .replace('/', '.') + val shortName = className.substringAfterLast('/') + .substringAfterLast('$') val kind = when (klass.kind) { ClassKind.INTERFACE -> JvmSymbolKind.INTERFACE @@ -128,23 +130,23 @@ object KotlinMetadataScanner { val supertypes = klass.supertypes.mapNotNull { supertype -> when (val c = supertype.classifier) { - is KmClassifier.Class -> c.name.replace('/', '.') + is KmClassifier.Class -> c.name else -> null } } symbols.add( JvmSymbol( - key = classFqName, + key = className, sourceId = sourceId, - fqName = classFqName, + name = className, shortName = shortName, packageName = packageName, kind = kind, language = JvmSourceLanguage.KOTLIN, visibility = kmVisibility(klass.visibility), data = JvmClassInfo( - supertypeFqNames = supertypes, + supertypeNames = supertypes, typeParameters = klass.typeParameters.map { it.name }, isAbstract = klass.modality == Modality.ABSTRACT, isFinal = klass.modality == Modality.FINAL, @@ -157,26 +159,26 @@ object KotlinMetadataScanner { ) for (fn in klass.functions) { - extractFunction(fn, classFqName, packageName, sourceId)?.let { symbols.add(it) } + extractFunction(fn, className, packageName, sourceId)?.let { symbols.add(it) } } for (prop in klass.properties) { - extractProperty(prop, classFqName, packageName, sourceId)?.let { symbols.add(it) } + extractProperty(prop, className, packageName, sourceId)?.let { symbols.add(it) } } if (kind == JvmSymbolKind.ENUM) { klass.kmEnumEntries.forEachIndexed { ordinal, entry -> symbols.add( JvmSymbol( - key = "$classFqName.$entry", + key = "$className#$entry", sourceId = sourceId, - fqName = "$classFqName.$entry", + name = "$className#$entry", shortName = entry.name, packageName = packageName, kind = JvmSymbolKind.ENUM_ENTRY, language = JvmSourceLanguage.KOTLIN, data = JvmEnumEntryInfo( - containingClassFqName = classFqName, + containingClassName = className, ordinal = ordinal, ), ) @@ -208,15 +210,15 @@ object KotlinMetadataScanner { JvmSymbol( key = fqName, sourceId = sourceId, - fqName = fqName, + name = fqName, shortName = alias.name, packageName = packageName, kind = JvmSymbolKind.TYPE_ALIAS, language = JvmSourceLanguage.KOTLIN, visibility = kmVisibility(alias.visibility), data = JvmTypeAliasInfo( - expandedTypeFqName = kmTypeToFqName(alias.expandedType), - expandedTypeDisplay = kmTypeToDisplay(alias.expandedType), + expandedTypeName = kmTypeToName(alias.expandedType), + expandedTypeDisplayName = kmTypeToDisplayName(alias.expandedType), typeParameters = alias.typeParameters.map { it.name }, ), ) @@ -242,44 +244,44 @@ object KotlinMetadataScanner { val parameters = fn.valueParameters.map { param -> JvmParameterInfo( name = param.name, - typeFqName = kmTypeToFqName(param.type), - typeDisplay = kmTypeToDisplay(param.type), + typeName = kmTypeToName(param.type), + typeDisplayName = kmTypeToDisplayName(param.type), hasDefaultValue = param.declaresDefaultValue, isVararg = param.varargElementType != null, ) } - val baseFqName = if (containingClass.isNotEmpty()) - "$containingClass.${fn.name}" else "$packageName.${fn.name}" - val key = "$baseFqName(${parameters.joinToString(",") { it.typeFqName }})" + val name = if (containingClass.isNotEmpty()) + "$containingClass#${fn.name}" else "$packageName#${fn.name}" + val key = "$name(${parameters.joinToString(",") { it.typeFqName }})" val signatureDisplay = buildString { append("(") - append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplay}" }) + append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplayName}" }) append("): ") - append(kmTypeToDisplay(fn.returnType)) + append(kmTypeToDisplayName(fn.returnType)) } return JvmSymbol( key = key, sourceId = sourceId, - fqName = baseFqName, + name = name, shortName = fn.name, packageName = packageName, kind = kind, language = JvmSourceLanguage.KOTLIN, visibility = vis, data = JvmFunctionInfo( - containingClassFqName = containingClass, - returnTypeFqName = kmTypeToFqName(fn.returnType), - returnTypeDisplay = kmTypeToDisplay(fn.returnType), + containingClassName = containingClass, + returnTypeName = kmTypeToName(fn.returnType), + returnTypeDisplayName = kmTypeToDisplayName(fn.returnType), parameterCount = parameters.size, parameters = parameters, signatureDisplay = signatureDisplay, typeParameters = fn.typeParameters.map { it.name }, kotlin = KotlinFunctionInfo( - receiverTypeFqName = receiverType?.let { kmTypeToFqName(it) } ?: "", - receiverTypeDisplay = receiverType?.let { kmTypeToDisplay(it) } ?: "", + receiverTypeName = receiverType?.let { kmTypeToName(it) } ?: "", + receiverTypeDisplayName = receiverType?.let { kmTypeToDisplayName(it) } ?: "", isSuspend = fn.isSuspend, isInline = fn.isInline, isInfix = fn.isInfix, @@ -306,25 +308,25 @@ object KotlinMetadataScanner { val isExtension = receiverType != null val kind = if (isExtension) JvmSymbolKind.EXTENSION_PROPERTY else JvmSymbolKind.PROPERTY - val fqName = if (containingClass.isNotEmpty()) - "$containingClass.${prop.name}" else "$packageName.${prop.name}" + val name = if (containingClass.isNotEmpty()) + "$containingClass#${prop.name}" else "$packageName#${prop.name}" return JvmSymbol( - key = fqName, + key = name, sourceId = sourceId, - fqName = fqName, + name = name, shortName = prop.name, packageName = packageName, kind = kind, language = JvmSourceLanguage.KOTLIN, visibility = vis, data = JvmFieldInfo( - containingClassFqName = containingClass, - typeFqName = kmTypeToFqName(prop.returnType), - typeDisplay = kmTypeToDisplay(prop.returnType), + containingClassName = containingClass, + typeName = kmTypeToName(prop.returnType), + typeDisplayName = kmTypeToDisplayName(prop.returnType), kotlin = KotlinPropertyInfo( - receiverTypeFqName = receiverType?.let { kmTypeToFqName(it) } ?: "", - receiverTypeDisplay = receiverType?.let { kmTypeToDisplay(it) } ?: "", + receiverTypeName = receiverType?.let { kmTypeToName(it) } ?: "", + receiverTypeDisplayName = receiverType?.let { kmTypeToDisplayName(it) } ?: "", isConst = prop.isConst, isLateinit = prop.isLateinit, hasGetter = prop.getter != null, @@ -336,15 +338,15 @@ object KotlinMetadataScanner { ) } - private fun kmTypeToFqName(type: KmType): String = when (val c = type.classifier) { - is KmClassifier.Class -> c.name.replace('/', '.') - is KmClassifier.TypeAlias -> c.name.replace('/', '.') + private fun kmTypeToName(type: KmType): String = when (val c = type.classifier) { + is KmClassifier.Class -> c.name + is KmClassifier.TypeAlias -> c.name is KmClassifier.TypeParameter -> "T${c.id}" } - private fun kmTypeToDisplay(type: KmType): String { - val base = kmTypeToFqName(type).substringAfterLast('.') - val args = type.arguments.mapNotNull { it.type?.let { t -> kmTypeToDisplay(t) } } + private fun kmTypeToDisplayName(type: KmType): String { + val base = kmTypeToDisplayName(type).substringAfterLast('.') + val args = type.arguments.mapNotNull { it.type?.let { t -> kmTypeToDisplayName(t) } } return buildString { append(base) if (args.isNotEmpty()) append("<${args.joinToString(", ")}>") 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 a925e45979..0c92e0ab8e 100644 --- a/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto +++ b/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto @@ -8,7 +8,7 @@ option java_multiple_files = false; message JvmSymbolData { - string fq_name = 1; + string name = 1; string short_name = 2; string package_name = 3; string source_id = 4; @@ -30,10 +30,10 @@ message JvmSymbolData { message ClassData { // FQN of the enclosing class (empty for top-level classes) - string containing_class_fq_name = 1; + string containing_class_name = 1; // Direct supertypes - repeated string supertype_fq_names = 2; + repeated string supertype_names = 2; // Type parameters: ["T", "R : Comparable"] repeated string type_parameters = 3; @@ -66,11 +66,11 @@ message KotlinClassData { message FunctionData { // FQN of the containing class (empty for top-level functions) - string containing_class_fq_name = 1; + string containing_class_name = 1; // Return type - string return_type_fq_name = 2; - string return_type_display = 3; + string return_type_name = 2; + string return_type_display_name = 3; // Parameters int32 parameter_count = 4; @@ -92,8 +92,8 @@ message FunctionData { message ParameterData { string name = 1; - string type_fq_name = 2; - string type_display = 3; + string type_name = 2; + string type_display_name = 3; bool has_default_value = 4; bool is_crossinline = 5; @@ -103,8 +103,8 @@ message ParameterData { message KotlinFunctionData { // Extension receiver type - string receiver_type_fq_name = 1; - string receiver_type_display = 2; + string receiver_type_name = 1; + string receiver_type_display_name = 2; // Modifiers bool is_suspend = 3; @@ -122,11 +122,11 @@ message KotlinFunctionData { message FieldData { // FQN of the containing class (empty for top-level properties) - string containing_class_fq_name = 1; + string containing_class_name = 1; // Type of the field/property - string type_fq_name = 2; - string type_display = 3; + string type_name = 2; + string type_display_name = 3; // Modifiers bool is_static = 4; @@ -140,8 +140,8 @@ message FieldData { message KotlinPropertyData { // Extension receiver type - string receiver_type_fq_name = 1; - string receiver_type_display = 2; + string receiver_type_name = 1; + string receiver_type_display_name = 2; bool is_const = 3; bool is_lateinit = 4; @@ -157,7 +157,7 @@ message KotlinPropertyData { message EnumEntryData { // FQN of the containing enum class - string containing_enum_fq_name = 1; + string containing_enum_name = 1; // Ordinal position int32 ordinal = 2; @@ -165,8 +165,8 @@ message EnumEntryData { message TypeAliasData { // The type this alias expands to - string expanded_type_fq_name = 1; - string expanded_type_display = 2; + string expanded_type_name = 1; + string expanded_type_display_name = 2; // Type parameters: ["T"] repeated string type_parameters = 3; 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 e1ca3b905f..9b1b1e25b3 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 @@ -332,9 +332,9 @@ private suspend fun KaSession.collectUnimportedSymbols( in JvmSymbolKind.CLASSIFIER_KINDS -> { val classInfo = symbol.data as JvmClassInfo - item.detail = symbol.fqName + item.detail = symbol.name item.setClassCompletionData( - className = symbol.fqName, + className = symbol.name, isNested = classInfo.isInner, topLevelClass = classInfo.containingClassFqName, ) From e6fcf6e7788e9b90912fade661222f005b71238f Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 9 Apr 2026 15:31:36 +0530 Subject: [PATCH 34/58] fix: filter-out ext syms with inapplicable receivers Signed-off-by: Akash Yadav --- .../kotlin/completion/KotlinCompletions.kt | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) 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 9b1b1e25b3..409f85f946 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 @@ -50,6 +50,8 @@ 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.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtQualifiedExpression @@ -303,8 +305,22 @@ private suspend fun KaSession.collectUnimportedSymbols( return@collect } - // TODO: filter-out callables with a receiver type whose receiver - // is not an implicit receiver at the current use-site + 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 + } + } val item = ktCompletionItem( name = symbol.shortName, @@ -348,6 +364,17 @@ private suspend fun KaSession.collectUnimportedSymbols( } } +private fun internalNameToClassId(internalName: String): ClassId { + val isLocal = false + val packageName = internalName.substringBeforeLast('/') + val relativeName = internalName.substringAfterLast('/') + return ClassId( + packageFqName = FqName.fromSegments(packageName.split('.')), + relativeClassName = FqName.fromSegments(relativeName.split('$')), + isLocal = isLocal + ) +} + context(ctx: AnalysisContext) private fun KaSession.collectKeywordCompletions( to: MutableList, From 9fa3d4a1b2189eff187ecc0d2a30662010b47dd9 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 9 Apr 2026 19:38:10 +0530 Subject: [PATCH 35/58] feat: add Kotlin source file index Signed-off-by: Akash Yadav --- .../{PersistentIndex.kt => SQLiteIndex.kt} | 9 +- lsp/jvm-symbol-index/build.gradle.kts | 1 + .../indexing/jvm/JvmLibrarySymbolIndex.kt | 6 +- .../jvm/KotlinSourceIndexingService.kt | 162 +++++++++++++++ .../indexing/jvm/KotlinSourceScanner.kt | 195 ++++++++++++++++++ .../indexing/jvm/KotlinSourceSymbolIndex.kt | 173 ++++++++++++++++ .../lsp/kotlin/KotlinLanguageServer.kt | 8 + .../kotlin/compiler/CompilationEnvironment.kt | 4 + .../lsp/kotlin/compiler/KotlinProjectModel.kt | 8 + .../kotlin/completion/KotlinCompletions.kt | 141 +++++++------ 10 files changed, 637 insertions(+), 70 deletions(-) rename lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/{PersistentIndex.kt => SQLiteIndex.kt} (97%) create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceIndexingService.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceSymbolIndex.kt 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 97% 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..8d885e533a 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 @@ -18,7 +18,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]: * ``` @@ -44,14 +44,15 @@ import kotlin.collections.iterator * @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, + dbName: String?, override val name: String = "persistent:${descriptor.name}", private val batchSize: Int = 500, ) : Index { 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/JvmLibrarySymbolIndex.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibrarySymbolIndex.kt index ec52e5d633..c961203f11 100644 --- 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 @@ -5,7 +5,7 @@ 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.SQLiteIndex 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 @@ -19,7 +19,7 @@ import java.io.Closeable */ class JvmLibrarySymbolIndex private constructor( /** Persistent cache — stores every JAR ever indexed. */ - val libraryCache: PersistentIndex, + val libraryCache: SQLiteIndex, /** Filtered view — only shows JARs on the current classpath. */ val libraryView: FilteredIndex, @@ -37,7 +37,7 @@ class JvmLibrarySymbolIndex private constructor( context: Context, dbName: String = DB_NAME_DEFAULT, ): JvmLibrarySymbolIndex { - val cache = PersistentIndex( + val cache = SQLiteIndex( descriptor = JvmSymbolDescriptor, context = context, dbName = dbName, diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceIndexingService.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceIndexingService.kt new file mode 100644 index 0000000000..2630d4ac2c --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceIndexingService.kt @@ -0,0 +1,162 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import android.content.Context +import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent +import com.itsaky.androidide.eventbus.events.file.FileCreationEvent +import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent +import com.itsaky.androidide.eventbus.events.file.FileRenameEvent +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.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.slf4j.LoggerFactory +import java.io.File + +/** + * Well-known registry key for the Kotlin source symbol index. + */ +val KOTLIN_SOURCE_SYMBOL_INDEX = IndexKey("kotlin-source-symbols") + +/** + * [IndexingService] that scans all Kotlin source files in the open project and + * maintains an in-memory [KotlinSourceSymbolIndex]. + */ +class KotlinSourceIndexingService( + private val context: Context, +) : IndexingService { + + companion object { + const val ID = "kotlin-source-indexing-service" + private val log = LoggerFactory.getLogger(KotlinSourceIndexingService::class.java) + } + + override val id = ID + override val providedKeys = listOf(KOTLIN_SOURCE_SYMBOL_INDEX) + + private var index: KotlinSourceSymbolIndex? = null + private val refreshMutex = Mutex() + private val coroutineScope = CoroutineScope(Dispatchers.Default) + + override suspend fun initialize(registry: IndexRegistry) { + val sourceIndex = KotlinSourceSymbolIndex.create(context) + this.index = sourceIndex + registry.register(KOTLIN_SOURCE_SYMBOL_INDEX, sourceIndex) + + if (!EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().register(this) + } + + log.info("Kotlin source symbol index initialized") + } + + override fun close() { + EventBus.getDefault().unregister(this) + coroutineScope.cancelIfActive("Kotlin source indexing service closed") + index?.close() + index = null + } + + /** + * Scans all `.kt` source files across all project modules and indexes any + * file not yet present in the in-memory index. + */ + fun refresh() { + coroutineScope.launch { + refreshMutex.withLock { indexAllSourceFiles() } + } + } + + private suspend fun indexAllSourceFiles() { + val index = this.index ?: run { + log.warn("Kotlin source index not initialized; skipping refresh") + return + } + + val workspace = ProjectManagerImpl.getInstance().workspace ?: run { + log.warn("Workspace model not available; skipping Kotlin source scan") + return + } + + val sourceFiles = workspace.subProjects + .asSequence() + .filterIsInstance() + .flatMap { module -> module.getSourceDirectories().asSequence() } + .filter { it.exists() && it.isDirectory } + .flatMap { dir -> dir.walkTopDown().filter { it.isFile && it.extension == "kt" } } + .map { it.absolutePath } + .toList() + + log.info("Found {} Kotlin source files to index", sourceFiles.size) + + var submitted = 0 + for (filePath in sourceFiles) { + if (!index.isFileCached(filePath)) { + submitted++ + index.indexFile(filePath) + } + } + + if (submitted > 0) { + log.info("{} Kotlin source files submitted for background indexing", submitted) + } else { + log.info("All Kotlin source files already cached, nothing to index") + } + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("UNUSED") + fun onFileCreated(event: FileCreationEvent) { + if (!event.file.isKotlinSource) return + val filePath = event.file.absolutePath + log.debug("File created, indexing: {}", filePath) + index?.indexFile(filePath) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("UNUSED") + fun onFileSaved(event: DocumentSaveEvent) { + val filePath = event.savedFile.toAbsolutePath().toString() + if (!filePath.endsWith(".kt")) return + log.debug("File saved, re-indexing: {}", filePath) + index?.reindexFile(filePath) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("UNUSED") + fun onFileDeleted(event: FileDeletionEvent) { + if (!event.file.isKotlinSource) return + val filePath = event.file.absolutePath + log.debug("File deleted, removing from index: {}", filePath) + index?.removeFile(filePath) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + @Suppress("UNUSED") + fun onFileRenamed(event: FileRenameEvent) { + val oldPath = event.file.absolutePath + val newPath = event.newFile.absolutePath + + if (event.file.isKotlinSource) { + log.debug("File renamed, removing old path from index: {}", oldPath) + index?.removeFile(oldPath) + } + + if (event.newFile.isKotlinSource) { + log.debug("File renamed, indexing new path: {}", newPath) + index?.indexFile(newPath) + } + } + + private val File.isKotlinSource: Boolean + get() = isFile && extension == "kt" +} diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt new file mode 100644 index 0000000000..b91327e6ca --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt @@ -0,0 +1,195 @@ +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.appdevforall.codeonthego.lsp.kotlin.index.FileIndex +import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbol +import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbolKind +import org.appdevforall.codeonthego.lsp.kotlin.parser.KotlinParser +import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolBuilder +import org.appdevforall.codeonthego.lsp.kotlin.symbol.Visibility +import org.slf4j.LoggerFactory +import java.io.File + +/** + * Parses a Kotlin source file and produces [JvmSymbol] entries for indexing. + * + * Uses tree-sitter (via [KotlinParser]) for fast, error-tolerant parsing and + * [SymbolBuilder] to extract declarations. The resulting symbols are represented + * using the shared [JvmSymbol] model so they can be stored in any + * [org.appdevforall.codeonthego.indexing.api.Index] that accepts [JvmSymbol]. + * + * Thread safety: each call to [scan] creates its own [KotlinParser] instance, + * so concurrent calls are safe. + */ +object KotlinSourceScanner { + + private val log = LoggerFactory.getLogger(KotlinSourceScanner::class.java) + + /** + * Parses the Kotlin source file at [filePath] and emits a [JvmSymbol] for + * each indexed declaration (classifiers, functions, properties, type aliases + * — both top-level and members). + * + * @param filePath Absolute path to the `.kt` file on disk. + * @param sourceId The [JvmSymbol.sourceId] to stamp on every emitted symbol. + * Typically the same as [filePath] so that [removeBySource] + * can remove all symbols from a specific file atomically. + */ + fun scan(filePath: String, sourceId: String): Flow = flow { + val file = File(filePath) + if (!file.exists() || !file.isFile) return@flow + + val content = try { + file.readText() + } catch (e: Exception) { + log.warn("Failed to read source file: {}", filePath, e) + return@flow + } + + KotlinParser().use { parser -> + val result = parser.parse(content, filePath) + result.tree.use { syntaxTree -> + val symbolTable = SymbolBuilder.build(syntaxTree, filePath) + val fileIndex = FileIndex.fromSymbolTable(symbolTable) + + // findByPrefix("", 0) returns all symbols because every name starts with "". + val allSymbols = fileIndex.findByPrefix("", 0) + for (symbol in allSymbols) { + toJvmSymbol(symbol, sourceId)?.let { emit(it) } + } + } + } + }.flowOn(Dispatchers.IO) + + private fun toJvmSymbol(symbol: IndexedSymbol, sourceId: String): JvmSymbol? { + val kind = mapKind(symbol) ?: return null + val visibility = mapVisibility(symbol.visibility) + + val data: JvmSymbolInfo = when { + symbol.kind.isClass -> JvmClassInfo( + internalName = symbol.fqName, + containingClassName = symbol.containingClass ?: "", + supertypeNames = symbol.superTypes, + typeParameters = symbol.typeParameters, + kotlin = KotlinClassInfo( + isData = symbol.kind == IndexedSymbolKind.DATA_CLASS, + isValue = symbol.kind == IndexedSymbolKind.VALUE_CLASS, + ), + ) + + symbol.kind == IndexedSymbolKind.FUNCTION + || symbol.kind == IndexedSymbolKind.CONSTRUCTOR -> { + val params = symbol.parameters.map { param -> + JvmParameterInfo( + name = param.name, + typeName = param.type, + typeDisplayName = param.type, + hasDefaultValue = param.hasDefault, + isVararg = param.isVararg, + ) + } + JvmFunctionInfo( + containingClassName = symbol.containingClass ?: "", + returnTypeName = symbol.returnType ?: "Unit", + returnTypeDisplayName = symbol.returnType ?: "Unit", + parameterCount = params.size, + parameters = params, + signatureDisplay = buildSignatureDisplay(symbol), + typeParameters = symbol.typeParameters, + kotlin = symbol.receiverType?.let { receiverType -> + KotlinFunctionInfo( + receiverTypeName = receiverType, + receiverTypeDisplayName = receiverType, + ) + }, + ) + } + + symbol.kind == IndexedSymbolKind.PROPERTY -> JvmFieldInfo( + containingClassName = symbol.containingClass ?: "", + typeName = symbol.returnType ?: "Any", + typeDisplayName = symbol.returnType ?: "Any", + kotlin = symbol.receiverType?.let { receiverType -> + KotlinPropertyInfo( + receiverTypeName = receiverType, + receiverTypeDisplayName = receiverType, + ) + }, + ) + + symbol.kind == IndexedSymbolKind.TYPE_ALIAS -> JvmTypeAliasInfo( + expandedTypeName = symbol.returnType ?: "", + expandedTypeDisplayName = symbol.returnType ?: "", + typeParameters = symbol.typeParameters, + ) + + else -> return null + } + + val key = when { + kind == JvmSymbolKind.FUNCTION + || kind == JvmSymbolKind.EXTENSION_FUNCTION + || kind == JvmSymbolKind.CONSTRUCTOR -> { + val paramTypes = symbol.parameters.joinToString(",") { it.type } + "${symbol.fqName}($paramTypes)" + } + else -> symbol.fqName + } + + return JvmSymbol( + key = key, + sourceId = sourceId, + name = symbol.fqName, + shortName = symbol.name, + packageName = symbol.packageName, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + isDeprecated = symbol.deprecated, + data = data, + ) + } + + private fun mapKind(symbol: IndexedSymbol): JvmSymbolKind? = when (symbol.kind) { + IndexedSymbolKind.CLASS -> JvmSymbolKind.CLASS + IndexedSymbolKind.INTERFACE -> JvmSymbolKind.INTERFACE + IndexedSymbolKind.OBJECT -> JvmSymbolKind.OBJECT + IndexedSymbolKind.ENUM_CLASS -> JvmSymbolKind.ENUM + IndexedSymbolKind.ANNOTATION_CLASS -> JvmSymbolKind.ANNOTATION_CLASS + IndexedSymbolKind.DATA_CLASS -> JvmSymbolKind.DATA_CLASS + IndexedSymbolKind.VALUE_CLASS -> JvmSymbolKind.VALUE_CLASS + IndexedSymbolKind.FUNCTION -> { + if (symbol.receiverType != null) JvmSymbolKind.EXTENSION_FUNCTION + else JvmSymbolKind.FUNCTION + } + IndexedSymbolKind.CONSTRUCTOR -> JvmSymbolKind.CONSTRUCTOR + IndexedSymbolKind.PROPERTY -> { + if (symbol.receiverType != null) JvmSymbolKind.EXTENSION_PROPERTY + else JvmSymbolKind.PROPERTY + } + IndexedSymbolKind.TYPE_ALIAS -> JvmSymbolKind.TYPE_ALIAS + } + + private fun mapVisibility(visibility: Visibility): JvmVisibility = when (visibility) { + Visibility.PUBLIC -> JvmVisibility.PUBLIC + Visibility.PROTECTED -> JvmVisibility.PROTECTED + Visibility.INTERNAL -> JvmVisibility.INTERNAL + Visibility.PRIVATE -> JvmVisibility.PRIVATE + } + + private fun buildSignatureDisplay(symbol: IndexedSymbol): String = buildString { + symbol.receiverType?.let { append(it).append('.') } + if (symbol.typeParameters.isNotEmpty()) { + append('<') + append(symbol.typeParameters.joinToString()) + append('>') + } + append('(') + append(symbol.parameters.joinToString { "${it.name}: ${it.type}" }) + append(')') + symbol.returnType?.let { append(": ").append(it) } + } +} diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceSymbolIndex.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceSymbolIndex.kt new file mode 100644 index 0000000000..f888fdfed5 --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceSymbolIndex.kt @@ -0,0 +1,173 @@ +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.SQLiteIndex +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 extracted from Kotlin source files in the project. + * + * Unlike [JvmLibrarySymbolIndex], which accumulates a persistent on-disk cache + * of library JARs across IDE sessions, this index is deliberately **in-memory**: + * it is rebuilt from scratch on each project open and discarded when the project + * closes. This is correct because source files are cheap to re-parse (tree-sitter + * is fast) and the index must always reflect the current on-disk state. + */ +class KotlinSourceSymbolIndex private constructor( + val sourceIndex: SQLiteIndex, + val sourceIndexer: BackgroundIndexer, +) : Closeable { + + companion object { + + const val INDEX_NAME_SOURCE = "kotlin-source-index" + + /** + * Creates a [KotlinSourceSymbolIndex] 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 in this case. + */ + fun create(context: Context): KotlinSourceSymbolIndex { + // dbName = null → AndroidX SQLiteOpenHelper creates an in-memory database. + val index = SQLiteIndex( + descriptor = JvmSymbolDescriptor, + context = context, + dbName = null, + name = INDEX_NAME_SOURCE, + ) + val indexer = BackgroundIndexer(index) + return KotlinSourceSymbolIndex( + sourceIndex = index, + sourceIndexer = indexer, + ) + } + } + + /** + * Indexes the symbols in [filePath], skipping the file if it was already + * indexed in this session. + * + * Use [reindexFile] to force re-parsing (e.g. after a save event). + */ + fun indexFile( + filePath: String, + provider: (sourceId: String) -> Flow = { sourceId -> + KotlinSourceScanner.scan(filePath, sourceId) + }, + ) = sourceIndexer.indexSource(filePath, skipIfExists = true, provider) + + /** + * Re-indexes [filePath] unconditionally, removing any previously indexed + * symbols for that file first. + * + * Call this after the file is saved to disk. + */ + fun reindexFile( + filePath: String, + provider: (sourceId: String) -> Flow = { sourceId -> + KotlinSourceScanner.scan(filePath, sourceId) + }, + ) = sourceIndexer.indexSource(filePath, skipIfExists = false, provider) + + /** + * Removes all symbols that originate from [filePath] from the index. + * + * Implemented by scheduling an indexing job with an empty provider so that + * the [BackgroundIndexer] properly cancels any in-flight job for the same + * source before clearing the entries. + */ + fun removeFile(filePath: String) { + sourceIndexer.indexSource( + sourceId = filePath, + skipIfExists = false, + ) { kotlinx.coroutines.flow.emptyFlow() } + } + + /** + * Returns `true` if [filePath] has already been indexed in this session. + */ + suspend fun isFileCached(filePath: String): Boolean = + sourceIndex.containsSource(filePath) + + /** Prefix-based completion across all source symbols. */ + fun findByPrefix(prefix: String, limit: Int = 200): Flow = + sourceIndex.query(indexQuery { prefix(KEY_NAME, prefix); this.limit = limit }) + + /** Prefix-based completion filtered to specific [kinds]. */ + fun findByPrefix( + prefix: String, + kinds: Set, + limit: Int = 200, + ): Flow = + sourceIndex.query(indexQuery { prefix(KEY_NAME, prefix); this.limit = 0 }) + .filter { it.kind in kinds } + .take(limit) + + /** Find extension functions / properties declared for [receiverTypeFqName]. */ + fun findExtensionsFor( + receiverTypeFqName: String, + namePrefix: String = "", + limit: Int = 200, + ): Flow = sourceIndex.query(indexQuery { + eq(KEY_RECEIVER_TYPE, receiverTypeFqName) + if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) + this.limit = limit + }) + + /** Top-level callable symbols (functions, properties) in a package. */ + fun findTopLevelCallablesInPackage( + packageName: String, + namePrefix: String = "", + limit: Int = 200, + ): Flow = sourceIndex.query(indexQuery { + eq(KEY_PACKAGE, packageName) + if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) + this.limit = 0 + }).filter { it.kind.isCallable && it.isTopLevel }.take(limit) + + /** Top-level classifier symbols (classes, interfaces, objects…) in a package. */ + fun findClassifiersInPackage( + packageName: String, + namePrefix: String = "", + limit: Int = 200, + ): Flow = sourceIndex.query(indexQuery { + eq(KEY_PACKAGE, packageName) + if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) + this.limit = 0 + }).filter { it.kind.isClassifier }.take(limit) + + /** Members of a specific class (functions, properties). */ + fun findMembersOf( + classFqName: String, + namePrefix: String = "", + limit: Int = 200, + ): Flow = sourceIndex.query(indexQuery { + eq(KEY_CONTAINING_CLASS, classFqName) + if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) + this.limit = limit + }) + + /** Point lookup by fully-qualified name. */ + suspend fun findByFqName(fqName: String): JvmSymbol? = sourceIndex.get(fqName) + + /** All distinct package names present in the index. */ + fun allPackages(): Flow = sourceIndex.distinctValues(KEY_PACKAGE) + + /** Suspends until all in-flight indexing jobs complete. */ + suspend fun awaitIndexing() = sourceIndexer.awaitAll() + + override fun close() { + sourceIndexer.close() + sourceIndex.close() + } +} 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..394d7cdcaf 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 @@ -57,6 +57,7 @@ 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.KotlinSourceIndexingService import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -104,6 +105,10 @@ class KotlinLanguageServer : ILanguageServer { if (!EventBus.getDefault().isRegistered(this)) { EventBus.getDefault().register(this) } + + ProjectManagerImpl.getInstance().indexingServiceManager.register( + KotlinSourceIndexingService(context = BaseApplication.baseInstance) + ) } override fun shutdown() { @@ -128,8 +133,11 @@ class KotlinLanguageServer : ILanguageServer { .indexingServiceManager val jvmIndexingService = indexingServiceManager.getService(JvmIndexingService.ID) as? JvmIndexingService? + val kotlinSourceIndexingService = + indexingServiceManager.getService(KotlinSourceIndexingService.ID) as? KotlinSourceIndexingService? jvmIndexingService?.refresh() + kotlinSourceIndexingService?.refresh() val jdkHome = Environment.JAVA_HOME.toPath() val jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE 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..b747556854 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 @@ -3,6 +3,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler import com.itsaky.androidide.lsp.kotlin.KtFileManager import com.itsaky.androidide.lsp.kotlin.utils.SymbolVisibilityChecker import org.appdevforall.codeonthego.indexing.jvm.JvmLibrarySymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KotlinSourceSymbolIndex import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory @@ -95,6 +96,9 @@ internal class CompilationEnvironment( val requireLibraryIndex: JvmLibrarySymbolIndex get() = checkNotNull(libraryIndex) + val sourceIndex: KotlinSourceSymbolIndex? + get() = project.sourceIndex + private val envMessageCollector = object : MessageCollector { override fun clear() { } 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..e4f70f35e4 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 @@ -8,6 +8,8 @@ import com.itsaky.androidide.projects.api.Workspace import com.itsaky.androidide.projects.models.bootClassPaths import org.appdevforall.codeonthego.indexing.jvm.JVM_LIBRARY_SYMBOL_INDEX import org.appdevforall.codeonthego.indexing.jvm.JvmLibrarySymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KOTLIN_SOURCE_SYMBOL_INDEX +import org.appdevforall.codeonthego.indexing.jvm.KotlinSourceSymbolIndex 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 @@ -53,6 +55,12 @@ internal class KotlinProjectModel { .registry .get(JVM_LIBRARY_SYMBOL_INDEX) + val sourceIndex: KotlinSourceSymbolIndex? + get() = ProjectManagerImpl.getInstance() + .indexingServiceManager + .registry + .get(KOTLIN_SOURCE_SYMBOL_INDEX) + /** * The kind of change that occurred. */ 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..dfb8f2ae8c 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 @@ -22,6 +22,7 @@ import org.appdevforall.codeonthego.indexing.jvm.JvmFunctionInfo import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolKind import org.appdevforall.codeonthego.indexing.jvm.JvmTypeAliasInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmVisibility import org.jetbrains.kotlin.analysis.api.KaContextParameterApi import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.KaIdeApi @@ -276,92 +277,106 @@ context(env: CompilationEnvironment, ctx: AnalysisContext) private suspend 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 } - val librarySymbolIndex = env.libraryIndex - if (librarySymbolIndex == null) { - logger.warn("Unable to find JVM library symbol index") - return - } + env.libraryIndex?.findByPrefix(ctx.partial) + ?.collect { symbol -> + val isVisible = visibilityChecker.isVisible( + symbol = symbol, + useSiteModule = useSiteModule, + useSitePackage = currentPackage, + ) + if (!isVisible) return@collect + buildUnimportedSymbolItem(symbol)?.let { to += it } + } + + // Source symbols: project .kt files — skip private and same-package symbols + env.sourceIndex?.findByPrefix(ctx.partial) + ?.collect { symbol -> + if (symbol.packageName == currentPackage) return@collect - 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 (symbol.kind.isCallable && !symbol.isTopLevel && !symbol.isExtension) { - // member-level, non-imported callable symbols should not be - // completed in scope completions - return@collect - } + buildUnimportedSymbolItem(symbol)?.let { to += it } + } +} + +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.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 + 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 null + } else return null + } + } - val item = ktCompletionItem( + val item = ktCompletionItem( + name = symbol.shortName, + kind = kindOf(symbol), + ) + + 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, - kind = kindOf(symbol), + hasParams = data.parameterCount > 0, ) + if (symbol.kind == JvmSymbolKind.CONSTRUCTOR) { + item.overrideTypeText = symbol.shortName + } + } - 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 - } - } - - JvmSymbolKind.TYPE_ALIAS -> { - item.detail = (symbol.data as JvmTypeAliasInfo).expandedTypeFqName - } + JvmSymbolKind.TYPE_ALIAS -> { + item.detail = (symbol.data as JvmTypeAliasInfo).expandedTypeFqName + } - 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, - ) - } + 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, + ) + } - else -> {} - } + else -> {} + } - logger.debug("Adding completion item: {}", item) - to += item - } + logger.debug("Adding completion item: {}", item) + return item } private fun internalNameToClassId(internalName: String): ClassId { From ab5c7d9bc51af97236e3c74affcc966aff979925 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 10 Apr 2026 18:21:22 +0530 Subject: [PATCH 36/58] fix: infinite loop when converting type names to display names Signed-off-by: Akash Yadav --- .../codeonthego/indexing/jvm/KotlinMetadataScanner.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..a0c6580600 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 @@ -345,7 +345,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) From 0ec7b4b12037eb52c44e6818ed2a32b7bedfce64 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 10 Apr 2026 18:22:26 +0530 Subject: [PATCH 37/58] fix: update ModuleResolver to resolve KaSourceModule from file path Signed-off-by: Akash Yadav --- .../lsp/kotlin/compiler/KotlinProjectModel.kt | 7 ++++++- .../lsp/kotlin/compiler/ModuleResolver.kt | 13 ++++++++++++- .../lsp/kotlin/completion/KotlinCompletions.kt | 4 ++-- 3 files changed, 20 insertions(+), 4 deletions(-) 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 e4f70f35e4..4069873445 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 @@ -155,6 +155,7 @@ internal class KotlinProjectModel { .associateWith(::addLibrary) val subprojectsAsModules = mutableMapOf() + val sourceRootToModuleMap = mutableMapOf() fun getOrCreateModule(project: ModuleProject): KaSourceModule { subprojectsAsModules[project]?.let { return it } @@ -186,12 +187,16 @@ internal class KotlinProjectModel { } subprojectsAsModules[project] = module + sourceRoots.forEach { root -> sourceRootToModuleMap[root] = module } return module } moduleProjects.forEach { addModule(getOrCreateModule(it)) } - val moduleResolver = ModuleResolver(jarMap = jarToModMap) + val moduleResolver = ModuleResolver( + jarMap = jarToModMap, + sourceRootMap = sourceRootToModuleMap, + ) _moduleResolver = moduleResolver _symbolVisibilityChecker = SymbolVisibilityChecker(moduleResolver) } 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 index 704d02978a..d1372a0852 100644 --- 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 @@ -2,24 +2,35 @@ 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.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule import org.slf4j.LoggerFactory import java.nio.file.Path import java.nio.file.Paths internal class ModuleResolver( private val jarMap: Map, + private val sourceRootMap: Map = emptyMap(), ) { companion object { private val logger = LoggerFactory.getLogger(ModuleResolver::class.java) } /** - * Find the module that declares the given source ID (JAR, source file, etc.) + * Find the module that declares the given source ID. + * + * - For library JARs, the source ID is the JAR path — looked up directly. + * - For source files, the source ID is the `.kt` file path — resolved by + * finding the source root directory that is an ancestor of that path. */ fun findDeclaringModule(sourceId: String): KaModule? { val path = Paths.get(sourceId) jarMap[path]?.let { return it } + // Walk source roots to find which module owns this file. + for ((root, module) in sourceRootMap) { + if (path.startsWith(root)) return module + } + return null } } \ No newline at end of file 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 dfb8f2ae8c..dd564eb93b 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 @@ -364,9 +364,9 @@ private fun KaSession.buildUnimportedSymbolItem(symbol: JvmSymbol): CompletionIt in JvmSymbolKind.CLASSIFIER_KINDS -> { val classInfo = symbol.data as JvmClassInfo - item.detail = symbol.name + item.detail = symbol.fqName item.setClassCompletionData( - className = symbol.name, + className = symbol.fqName, isNested = classInfo.isInner, topLevelClass = classInfo.containingClassFqName, ) From bf9b2da0c391d08e84b9b29c83276e27c28a889a Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 10 Apr 2026 18:22:43 +0530 Subject: [PATCH 38/58] fix: update KotlinSourceScanner to use internal names in index Signed-off-by: Akash Yadav --- .../codeonthego/indexing/jvm/JvmSymbol.kt | 3 + .../indexing/jvm/KotlinSourceScanner.kt | 626 +++++++++++++----- 2 files changed, 453 insertions(+), 176 deletions(-) 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/KotlinSourceScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt index b91327e6ca..3f5cb2725b 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt @@ -4,192 +4,466 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import org.appdevforall.codeonthego.lsp.kotlin.index.FileIndex -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbol -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbolKind +import org.appdevforall.codeonthego.indexing.jvm.KotlinSourceScanner.fqnToInternalName +import org.appdevforall.codeonthego.indexing.jvm.KotlinSourceScanner.scan import org.appdevforall.codeonthego.lsp.kotlin.parser.KotlinParser +import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassKind +import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol +import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol +import org.appdevforall.codeonthego.lsp.kotlin.symbol.PropertySymbol +import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolBuilder +import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeAliasSymbol +import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeReference import org.appdevforall.codeonthego.lsp.kotlin.symbol.Visibility import org.slf4j.LoggerFactory import java.io.File /** - * Parses a Kotlin source file and produces [JvmSymbol] entries for indexing. + * Parses a Kotlin source file and produces [JvmSymbol] entries for indexing, + * working directly from the [SymbolBuilder] output. * - * Uses tree-sitter (via [KotlinParser]) for fast, error-tolerant parsing and - * [SymbolBuilder] to extract declarations. The resulting symbols are represented - * using the shared [JvmSymbol] model so they can be stored in any - * [org.appdevforall.codeonthego.indexing.api.Index] that accepts [JvmSymbol]. + * Type references coming from source (which are as-written, dot-separated) + * are converted to internal names via [fqnToInternalName], which applies the + * standard Java naming convention: lowercase segments are package components, + * uppercase-starting segments are class components. * - * Thread safety: each call to [scan] creates its own [KotlinParser] instance, - * so concurrent calls are safe. + * Thread safety: each call to [scan] creates its own [KotlinParser] instance. */ object KotlinSourceScanner { - private val log = LoggerFactory.getLogger(KotlinSourceScanner::class.java) - - /** - * Parses the Kotlin source file at [filePath] and emits a [JvmSymbol] for - * each indexed declaration (classifiers, functions, properties, type aliases - * — both top-level and members). - * - * @param filePath Absolute path to the `.kt` file on disk. - * @param sourceId The [JvmSymbol.sourceId] to stamp on every emitted symbol. - * Typically the same as [filePath] so that [removeBySource] - * can remove all symbols from a specific file atomically. - */ - fun scan(filePath: String, sourceId: String): Flow = flow { - val file = File(filePath) - if (!file.exists() || !file.isFile) return@flow - - val content = try { - file.readText() - } catch (e: Exception) { - log.warn("Failed to read source file: {}", filePath, e) - return@flow - } - - KotlinParser().use { parser -> - val result = parser.parse(content, filePath) - result.tree.use { syntaxTree -> - val symbolTable = SymbolBuilder.build(syntaxTree, filePath) - val fileIndex = FileIndex.fromSymbolTable(symbolTable) - - // findByPrefix("", 0) returns all symbols because every name starts with "". - val allSymbols = fileIndex.findByPrefix("", 0) - for (symbol in allSymbols) { - toJvmSymbol(symbol, sourceId)?.let { emit(it) } - } - } - } - }.flowOn(Dispatchers.IO) - - private fun toJvmSymbol(symbol: IndexedSymbol, sourceId: String): JvmSymbol? { - val kind = mapKind(symbol) ?: return null - val visibility = mapVisibility(symbol.visibility) - - val data: JvmSymbolInfo = when { - symbol.kind.isClass -> JvmClassInfo( - internalName = symbol.fqName, - containingClassName = symbol.containingClass ?: "", - supertypeNames = symbol.superTypes, - typeParameters = symbol.typeParameters, - kotlin = KotlinClassInfo( - isData = symbol.kind == IndexedSymbolKind.DATA_CLASS, - isValue = symbol.kind == IndexedSymbolKind.VALUE_CLASS, - ), - ) - - symbol.kind == IndexedSymbolKind.FUNCTION - || symbol.kind == IndexedSymbolKind.CONSTRUCTOR -> { - val params = symbol.parameters.map { param -> - JvmParameterInfo( - name = param.name, - typeName = param.type, - typeDisplayName = param.type, - hasDefaultValue = param.hasDefault, - isVararg = param.isVararg, - ) - } - JvmFunctionInfo( - containingClassName = symbol.containingClass ?: "", - returnTypeName = symbol.returnType ?: "Unit", - returnTypeDisplayName = symbol.returnType ?: "Unit", - parameterCount = params.size, - parameters = params, - signatureDisplay = buildSignatureDisplay(symbol), - typeParameters = symbol.typeParameters, - kotlin = symbol.receiverType?.let { receiverType -> - KotlinFunctionInfo( - receiverTypeName = receiverType, - receiverTypeDisplayName = receiverType, - ) - }, - ) - } - - symbol.kind == IndexedSymbolKind.PROPERTY -> JvmFieldInfo( - containingClassName = symbol.containingClass ?: "", - typeName = symbol.returnType ?: "Any", - typeDisplayName = symbol.returnType ?: "Any", - kotlin = symbol.receiverType?.let { receiverType -> - KotlinPropertyInfo( - receiverTypeName = receiverType, - receiverTypeDisplayName = receiverType, - ) - }, - ) - - symbol.kind == IndexedSymbolKind.TYPE_ALIAS -> JvmTypeAliasInfo( - expandedTypeName = symbol.returnType ?: "", - expandedTypeDisplayName = symbol.returnType ?: "", - typeParameters = symbol.typeParameters, - ) - - else -> return null - } - - val key = when { - kind == JvmSymbolKind.FUNCTION - || kind == JvmSymbolKind.EXTENSION_FUNCTION - || kind == JvmSymbolKind.CONSTRUCTOR -> { - val paramTypes = symbol.parameters.joinToString(",") { it.type } - "${symbol.fqName}($paramTypes)" - } - else -> symbol.fqName - } - - return JvmSymbol( - key = key, - sourceId = sourceId, - name = symbol.fqName, - shortName = symbol.name, - packageName = symbol.packageName, - kind = kind, - language = JvmSourceLanguage.KOTLIN, - visibility = visibility, - isDeprecated = symbol.deprecated, - data = data, - ) - } - - private fun mapKind(symbol: IndexedSymbol): JvmSymbolKind? = when (symbol.kind) { - IndexedSymbolKind.CLASS -> JvmSymbolKind.CLASS - IndexedSymbolKind.INTERFACE -> JvmSymbolKind.INTERFACE - IndexedSymbolKind.OBJECT -> JvmSymbolKind.OBJECT - IndexedSymbolKind.ENUM_CLASS -> JvmSymbolKind.ENUM - IndexedSymbolKind.ANNOTATION_CLASS -> JvmSymbolKind.ANNOTATION_CLASS - IndexedSymbolKind.DATA_CLASS -> JvmSymbolKind.DATA_CLASS - IndexedSymbolKind.VALUE_CLASS -> JvmSymbolKind.VALUE_CLASS - IndexedSymbolKind.FUNCTION -> { - if (symbol.receiverType != null) JvmSymbolKind.EXTENSION_FUNCTION - else JvmSymbolKind.FUNCTION - } - IndexedSymbolKind.CONSTRUCTOR -> JvmSymbolKind.CONSTRUCTOR - IndexedSymbolKind.PROPERTY -> { - if (symbol.receiverType != null) JvmSymbolKind.EXTENSION_PROPERTY - else JvmSymbolKind.PROPERTY - } - IndexedSymbolKind.TYPE_ALIAS -> JvmSymbolKind.TYPE_ALIAS - } - - private fun mapVisibility(visibility: Visibility): JvmVisibility = when (visibility) { - Visibility.PUBLIC -> JvmVisibility.PUBLIC - Visibility.PROTECTED -> JvmVisibility.PROTECTED - Visibility.INTERNAL -> JvmVisibility.INTERNAL - Visibility.PRIVATE -> JvmVisibility.PRIVATE - } - - private fun buildSignatureDisplay(symbol: IndexedSymbol): String = buildString { - symbol.receiverType?.let { append(it).append('.') } - if (symbol.typeParameters.isNotEmpty()) { - append('<') - append(symbol.typeParameters.joinToString()) - append('>') - } - append('(') - append(symbol.parameters.joinToString { "${it.name}: ${it.type}" }) - append(')') - symbol.returnType?.let { append(": ").append(it) } - } + private val log = LoggerFactory.getLogger(KotlinSourceScanner::class.java) + + /** + * Parses the Kotlin source file at [filePath] and emits a [JvmSymbol] for + * each public/internal declaration found (classes and their members, + * top-level functions, properties, and type aliases). + */ + fun scan(filePath: String, sourceId: String): Flow = flow { + val file = File(filePath) + if (!file.exists() || !file.isFile) return@flow + + val content = try { + file.readText() + } catch (e: Exception) { + log.warn("Failed to read source file: {}", filePath, e) + return@flow + } + + KotlinParser().use { parser -> + val result = parser.parse(content, filePath) + result.tree.use { syntaxTree -> + val symbolTable = SymbolBuilder.build(syntaxTree, filePath) + // Internal prefix for this file's package: "com/example" + val pkgInternal = symbolTable.packageName.replace('.', '/') + + for (symbol in symbolTable.topLevelSymbols) { + for (jvmSymbol in toJvmSymbols( + symbol, + pkgInternal, + containingClass = "", + sourceId + )) { + emit(jvmSymbol) + } + } + } + } + }.flowOn(Dispatchers.IO) + + private fun toJvmSymbols( + symbol: Symbol, + pkgInternal: String, + containingClass: String, + sourceId: String, + ): List = when (symbol) { + is ClassSymbol -> classSymbols(symbol, pkgInternal, containingClass, sourceId) + is FunctionSymbol -> listOfNotNull( + functionSymbol( + symbol, + pkgInternal, + containingClass, + sourceId + ) + ) + + is PropertySymbol -> listOfNotNull( + propertySymbol( + symbol, + pkgInternal, + containingClass, + sourceId + ) + ) + + is TypeAliasSymbol -> listOfNotNull( + typeAliasSymbol( + symbol, + pkgInternal, + containingClass, + sourceId + ) + ) + + else -> emptyList() + } + + private fun classSymbols( + symbol: ClassSymbol, + pkgInternal: String, + containingClass: String, + sourceId: String, + ): List { + // Enum entries use a member-style key relative to their containing enum. + if (symbol.kind == ClassKind.ENUM_ENTRY) { + return listOf(enumEntrySymbol(symbol, pkgInternal, containingClass, sourceId)) + } + + val visibility = mapVisibility(symbol.visibility) + if (visibility == JvmVisibility.PRIVATE) return emptyList() + + // Internal name: "com/example/Outer$Inner" for nested, "com/example/Outer" for top-level. + val classInternalName = buildClassInternalName(symbol.name, pkgInternal, containingClass) + val packageName = pkgInternal.replace('/', '.') + + val kind = mapClassKind(symbol) + + val classSymbol = JvmSymbol( + key = classInternalName, + sourceId = sourceId, + name = classInternalName, + shortName = symbol.name, + packageName = packageName, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmClassInfo( + internalName = classInternalName, + containingClassName = containingClass, + supertypeNames = symbol.superTypes.map { fqnToInternalName(it.name) }, + typeParameters = symbol.typeParameters.map { it.name }, + isAbstract = symbol.modifiers.isAbstract, + isFinal = symbol.modifiers.isFinal, + isInner = symbol.modifiers.isInner, + kotlin = KotlinClassInfo( + isData = symbol.modifiers.isData, + isValue = symbol.modifiers.isValue, + isSealed = symbol.modifiers.isSealed, + ), + ), + ) + + val result = mutableListOf(classSymbol) + + // Primary constructor (not always in the member scope, depending on SymbolBuilder). + symbol.primaryConstructor?.let { ctor -> + if (!ctor.isPrimaryConstructor || ctor !in (symbol.memberScope?.allSymbols + ?: emptyList()) + ) { + functionSymbol(ctor, pkgInternal, classInternalName, sourceId)?.let { result += it } + } + } + + // All members: nested classes, secondary constructors, functions, properties. + for (member in symbol.memberScope?.allSymbols ?: emptyList()) { + result += toJvmSymbols(member, pkgInternal, classInternalName, sourceId) + } + + return result + } + + private fun enumEntrySymbol( + symbol: ClassSymbol, + pkgInternal: String, + containingClass: String, + sourceId: String, + ): JvmSymbol { + val packageName = pkgInternal.replace('/', '.') + return JvmSymbol( + key = "$containingClass#${symbol.name}", + sourceId = sourceId, + name = "$containingClass#${symbol.name}", + shortName = symbol.name, + packageName = packageName, + kind = JvmSymbolKind.ENUM_ENTRY, + language = JvmSourceLanguage.KOTLIN, + data = JvmEnumEntryInfo(containingClassName = containingClass), + ) + } + + private fun functionSymbol( + symbol: FunctionSymbol, + pkgInternal: String, + containingClass: String, + sourceId: String, + ): JvmSymbol? { + val visibility = mapVisibility(symbol.visibility) + if (visibility == JvmVisibility.PRIVATE) return null + + val kind = when { + symbol.isConstructor -> JvmSymbolKind.CONSTRUCTOR + symbol.isExtension -> JvmSymbolKind.EXTENSION_FUNCTION + else -> JvmSymbolKind.FUNCTION + } + + val packageName = pkgInternal.replace('/', '.') + val owner = containingClass.ifEmpty { pkgInternal } + + // For constructors, the short name is the class's simple name. + val shortName = if (symbol.isConstructor) { + containingClass.substringAfterLast('/').substringAfterLast('$') + } else { + symbol.name + } + val name = "$owner#$shortName" + + val parameters = symbol.parameters.map { param -> + JvmParameterInfo( + name = param.name, + typeName = param.type?.let { fqnToInternalName(it.name) } ?: "", + typeDisplayName = param.type?.render() ?: "", + hasDefaultValue = param.hasDefaultValue, + isVararg = param.isVararg, + isCrossinline = param.isCrossinline, + isNoinline = param.isNoinline, + ) + } + + val returnTypeInternal = + symbol.returnType?.let { fqnToInternalName(it.name) } ?: "kotlin/Unit" + val returnTypeDisplay = symbol.returnType?.render() ?: "Unit" + val receiverType = symbol.receiverType + + val key = "$name(${parameters.joinToString(",") { it.typeName }})" + + val signatureDisplay = buildString { + receiverType?.let { append(displayName(it)).append('.') } + if (symbol.typeParameters.isNotEmpty()) { + append('<') + append(symbol.typeParameters.joinToString { it.name }) + append('>') + } + append('(') + append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplayName}" }) + append(')') + if (!symbol.isConstructor) { + append(": ") + append(returnTypeDisplay) + } + } + + return JvmSymbol( + key = key, + sourceId = sourceId, + name = name, + shortName = shortName, + packageName = packageName, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmFunctionInfo( + containingClassName = containingClass, + returnTypeName = returnTypeInternal, + returnTypeDisplayName = returnTypeDisplay, + parameterCount = parameters.size, + parameters = parameters, + signatureDisplay = signatureDisplay, + typeParameters = symbol.typeParameters.map { it.name }, + isAbstract = symbol.modifiers.isAbstract, + isFinal = symbol.modifiers.isFinal, + kotlin = KotlinFunctionInfo( + receiverTypeName = receiverType?.let { fqnToInternalName(it.name) } ?: "", + receiverTypeDisplayName = receiverType?.let { displayName(it) } ?: "", + isSuspend = symbol.isSuspend, + isInline = symbol.isInline, + isInfix = symbol.isInfix, + isOperator = symbol.isOperator, + isTailrec = symbol.isTailrec, + isExternal = symbol.modifiers.isExternal, + isExpect = symbol.modifiers.isExpect, + isActual = symbol.modifiers.isActual, + isReturnTypeNullable = symbol.returnType?.isNullable ?: false, + ), + ), + ) + } + + private fun propertySymbol( + symbol: PropertySymbol, + pkgInternal: String, + containingClass: String, + sourceId: String, + ): JvmSymbol? { + val visibility = mapVisibility(symbol.visibility) + if (visibility == JvmVisibility.PRIVATE) return null + + val kind = + if (symbol.isExtension) JvmSymbolKind.EXTENSION_PROPERTY else JvmSymbolKind.PROPERTY + val packageName = pkgInternal.replace('/', '.') + val owner = containingClass.ifEmpty { pkgInternal } + val name = "$owner#${symbol.name}" + val receiverType = symbol.receiverType + + return JvmSymbol( + key = name, + sourceId = sourceId, + name = name, + shortName = symbol.name, + packageName = packageName, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmFieldInfo( + containingClassName = containingClass, + typeName = symbol.type?.let { fqnToInternalName(it.name) } ?: "", + typeDisplayName = symbol.type?.let { displayName(it) } ?: "", + isFinal = !symbol.isVar, + kotlin = KotlinPropertyInfo( + receiverTypeName = receiverType?.let { fqnToInternalName(it.name) } ?: "", + receiverTypeDisplayName = receiverType?.let { displayName(it) } ?: "", + isConst = symbol.isConst, + isLateinit = symbol.isLateInit, + hasGetter = symbol.hasCustomGetter, + hasSetter = symbol.hasCustomSetter, + isDelegated = symbol.isDelegated, + isExpect = symbol.modifiers.isExpect, + isActual = symbol.modifiers.isActual, + isExternal = symbol.modifiers.isExternal, + isTypeNullable = symbol.type?.isNullable ?: false, + ), + ), + ) + } + + private fun typeAliasSymbol( + symbol: TypeAliasSymbol, + pkgInternal: String, + containingClass: String, + sourceId: String, + ): JvmSymbol? { + val visibility = mapVisibility(symbol.visibility) + if (visibility == JvmVisibility.PRIVATE) return null + + val packageName = pkgInternal.replace('/', '.') + val internalName = buildClassInternalName(symbol.name, pkgInternal, containingClass) + + return JvmSymbol( + key = internalName, + sourceId = sourceId, + name = internalName, + shortName = symbol.name, + packageName = packageName, + kind = JvmSymbolKind.TYPE_ALIAS, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmTypeAliasInfo( + containingClassName = containingClass, + expandedTypeName = symbol.underlyingType?.let { fqnToInternalName(it.name) } ?: "", + expandedTypeDisplayName = symbol.underlyingType?.let { displayName(it) } ?: "", + typeParameters = symbol.typeParameters.map { it.name }, + ), + ) + } + + /** + * Builds the JVM internal name for a class. + * + * - Top-level: `com/example/MyClass` + * - Nested: `com/example/MyClass$Inner` + */ + private fun buildClassInternalName( + simpleName: String, + pkgInternal: String, + containingClass: String, + ): String = when { + containingClass.isNotEmpty() -> $$"$$containingClass$$$simpleName" + pkgInternal.isNotEmpty() -> "$pkgInternal/$simpleName" + else -> simpleName + } + + /** + * Converts a dot-separated FQN (as written in Kotlin source) to a JVM internal name. + * + * Applies standard Java naming conventions: lowercase-starting segments are + * package components (joined with `/`), uppercase-starting segments are class + * components (joined with `$`). + * + * Examples: + * ``` + * "String" → "String" + * "kotlin.String" → "kotlin/String" + * "java.util.Map" → "java/util/Map" + * "java.util.Map.Entry" → "java/util/Map$Entry" + * ``` + */ + internal fun fqnToInternalName(fqn: String): String { + if (fqn.isEmpty() || !fqn.contains('.')) return fqn + + val parts = fqn.split('.') + val pkg = mutableListOf() + val cls = mutableListOf() + + for (part in parts) { + if (cls.isEmpty() && part.isNotEmpty() && part[0].isLowerCase()) { + pkg += part + } else { + cls += part + } + } + + return when { + pkg.isEmpty() -> cls.joinToString("$") + cls.isEmpty() -> pkg.joinToString("/") + else -> "${pkg.joinToString("/")}/${cls.joinToString("$")}" + } + } + + /** + * Returns a short display name for a [TypeReference]: the simple class name + * with type arguments rendered, but without the package prefix. + * + * E.g. `java.util.Map.Entry` → `Entry`, `List` → `List`. + */ + private fun displayName(ref: TypeReference): String { + val baseName = fqnToInternalName(ref.name) + .substringAfterLast('/') + .substringAfterLast('$') + val args = ref.typeArguments + return buildString { + append(baseName) + if (args.isNotEmpty()) { + append('<') + append(args.joinToString(", ") { displayName(it) }) + append('>') + } + if (ref.isNullable) append('?') + } + } + + private fun mapClassKind(symbol: ClassSymbol): JvmSymbolKind = when (symbol.kind) { + ClassKind.CLASS -> when { + symbol.modifiers.isSealed -> JvmSymbolKind.SEALED_CLASS + else -> JvmSymbolKind.CLASS + } + + ClassKind.INTERFACE -> when { + symbol.modifiers.isSealed -> JvmSymbolKind.SEALED_INTERFACE + else -> JvmSymbolKind.INTERFACE + } + + ClassKind.OBJECT -> JvmSymbolKind.OBJECT + ClassKind.COMPANION_OBJECT -> JvmSymbolKind.COMPANION_OBJECT + ClassKind.ENUM_CLASS -> JvmSymbolKind.ENUM + ClassKind.ENUM_ENTRY -> JvmSymbolKind.ENUM_ENTRY + ClassKind.ANNOTATION_CLASS -> JvmSymbolKind.ANNOTATION_CLASS + ClassKind.DATA_CLASS -> JvmSymbolKind.DATA_CLASS + ClassKind.VALUE_CLASS -> JvmSymbolKind.VALUE_CLASS + } + + private fun mapVisibility(visibility: Visibility): JvmVisibility = when (visibility) { + Visibility.PUBLIC -> JvmVisibility.PUBLIC + Visibility.PROTECTED -> JvmVisibility.PROTECTED + Visibility.INTERNAL -> JvmVisibility.INTERNAL + Visibility.PRIVATE -> JvmVisibility.PRIVATE + } } From 49e7efe182b84d5c8451c9a2c0e07e1c19b13fb9 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Mon, 13 Apr 2026 19:58:51 +0530 Subject: [PATCH 39/58] feat: add custom implementations of analysis API services Signed-off-by: Akash Yadav --- common/build.gradle.kts | 2 + .../itsaky/androidide/utils/Environment.java | 5 + .../javac/config/JavacConfigProvider.java | 6 +- .../logging/IDELoggingConfigurator.kt | 1 - .../codeonthego/indexing/FilteredIndex.kt | 12 +- .../codeonthego/indexing/InMemoryIndex.kt | 35 +- .../codeonthego/indexing/MergedIndex.kt | 49 +- .../codeonthego/indexing/SQLiteIndex.kt | 64 +-- .../codeonthego/indexing/api/Index.kt | 28 +- .../indexing/util/BackgroundIndexer.kt | 93 +--- .../androidide/lsp/java/JavaLanguageServer.kt | 6 +- .../indexing/jvm/CombinedJarScanner.kt | 11 +- ...ervice.kt => JvmLibraryIndexingService.kt} | 33 +- .../indexing/jvm/JvmLibrarySymbolIndex.kt | 166 ------- .../indexing/jvm/JvmSymbolIndex.kt | 143 ++++++ .../jvm/KotlinSourceIndexingService.kt | 162 ------ .../indexing/jvm/KotlinSourceScanner.kt | 469 ------------------ .../indexing/jvm/KotlinSourceSymbolIndex.kt | 173 ------- .../indexing/jvm/KtFileMetadata.kt | 47 ++ .../indexing/jvm/KtFileMetadataDescriptor.kt | 64 +++ .../indexing/jvm/KtFileMetadataIndex.kt | 141 ++++++ .../src/main/proto/jvm_symbol.proto | 9 + .../lsp/kotlin/KotlinLanguageServer.kt | 70 +-- .../androidide/lsp/kotlin/KtFileManager.kt | 208 -------- .../kotlin/compiler/CompilationEnvironment.kt | 385 +++++++++----- .../lsp/kotlin/compiler/Compiler.kt | 7 +- .../lsp/kotlin/compiler/KotlinProjectModel.kt | 138 +----- .../lsp/kotlin/compiler/ModuleResolver.kt | 36 -- .../lsp/kotlin/compiler/ReadWriteLock.kt | 24 + .../lsp/kotlin/compiler/WorkspaceExts.kt | 93 ++++ .../lsp/kotlin/compiler/index/IndexCommand.kt | 14 + .../lsp/kotlin/compiler/index/IndexWorker.kt | 120 +++++ .../kotlin/compiler/index/KtSymbolIndex.kt | 140 ++++++ .../kotlin/compiler/index/ScanningWorker.kt | 57 +++ .../compiler/index/SourceFileIndexer.kt | 414 ++++++++++++++++ .../lsp/kotlin/compiler/index/WorkerQueue.kt | 27 + .../compiler/modules/AbstractKtModule.kt | 29 ++ .../compiler/modules/KtLibraryModule.kt | 133 +++++ .../lsp/kotlin/compiler/modules/KtModule.kt | 39 ++ .../kotlin/compiler/modules/KtSourceModule.kt | 110 ++++ .../modules/NotUnderContentRootModule.kt | 40 ++ .../compiler/registrar/LspServiceRegistrar.kt | 122 +++++ .../services/AnalysisPermissionOptions.kt | 8 + .../compiler/services/AnnotationsResolver.kt | 165 ++++++ .../compiler/services/DeclarationsProvider.kt | 189 +++++++ .../services/DirectInheritorsProvider.kt | 172 +++++++ .../JavaModuleAccessibilityChecker.kt | 33 ++ .../services/JavaModuleAnnotationsProvider.kt | 16 + .../kotlin/compiler/services/KtLspService.kt | 16 + .../compiler/services/LanguageSettings.kt | 9 + .../services/ModificationTrackerFactory.kt | 6 + .../services/ModuleDependentsProvider.kt | 67 +++ .../compiler/services/PackageProvider.kt | 85 ++++ .../compiler/services/PlatformSettings.kt | 9 + .../services/ProjectStructureProvider.kt | 113 +++++ .../compiler/services/WriteAccessGuard.kt | 11 + .../completion/AdvancedKotlinEditHandler.kt | 6 +- .../KotlinClassImportEditHandler.kt | 4 +- .../kotlin/completion/KotlinCompletions.kt | 29 +- .../diagnostic/KotlinDiagnosticProvider.kt | 44 +- .../kotlin/utils/SymbolVisibilityChecker.kt | 6 +- .../main/resources/META-INF/kt-lsp/kt-lsp.xml | 12 + .../src/main/proto/android.proto | 4 + .../src/main/proto/common.proto | 9 + .../project-models/src/main/proto/java.proto | 3 + .../tooling/impl/serial/AndroidProjectExts.kt | 1 + .../tooling/impl/serial/JavaProjectExts.kt | 1 + 67 files changed, 3163 insertions(+), 1780 deletions(-) rename lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/{JvmIndexingService.kt => JvmLibraryIndexingService.kt} (82%) delete mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibrarySymbolIndex.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt delete mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceIndexingService.kt delete mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt delete mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceSymbolIndex.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadata.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataDescriptor.kt create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataIndex.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KtFileManager.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ModuleResolver.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspServiceRegistrar.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt create mode 100644 lsp/kotlin/src/main/resources/META-INF/kt-lsp/kt-lsp.xml 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 a3e1e64637..b6d69b1f28 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; @@ -188,6 +190,9 @@ public static void init(Context context) { NDK_DIR = new File(ANDROID_HOME, "ndk"); + // 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..1aced73afb 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,12 +75,10 @@ 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 } } @@ -95,7 +91,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 +105,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/SQLiteIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt index 8d885e533a..786dca5c03 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.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 @@ -41,6 +38,11 @@ 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). @@ -53,7 +55,7 @@ class SQLiteIndex( override val descriptor: IndexDescriptor, context: Context, dbName: String?, - override val name: String = "persistent:${descriptor.name}", + override val name: String = "sqlite:${descriptor.name}", private val batchSize: Int = 500, ) : Index { @@ -100,18 +102,18 @@ class SQLiteIndex( 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( @@ -134,41 +136,17 @@ class SQLiteIndex( 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/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 f5c1fb627c..995944e565 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 @@ -74,7 +74,7 @@ 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.JvmLibraryIndexingService import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -121,7 +121,7 @@ class JavaLanguageServer : ILanguageServer { val projectManager = ProjectManagerImpl.getInstance() projectManager.indexingServiceManager.register( - service = JvmIndexingService(context = BaseApplication.baseInstance) + service = JvmLibraryIndexingService(context = BaseApplication.baseInstance) ) JavaSnippetRepository.init() @@ -159,7 +159,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/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..64af7ce982 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,5 @@ 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.org.objectweb.asm.AnnotationVisitor import org.jetbrains.org.objectweb.asm.ClassReader import org.jetbrains.org.objectweb.asm.ClassVisitor @@ -22,12 +18,12 @@ object CombinedJarScanner { private val log = LoggerFactory.getLogger(CombinedJarScanner::class.java) - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { + 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 +46,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/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 c961203f11..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.SQLiteIndex -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: SQLiteIndex, - - /** 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 = SQLiteIndex( - 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/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/KotlinSourceIndexingService.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceIndexingService.kt deleted file mode 100644 index 2630d4ac2c..0000000000 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceIndexingService.kt +++ /dev/null @@ -1,162 +0,0 @@ -package org.appdevforall.codeonthego.indexing.jvm - -import android.content.Context -import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent -import com.itsaky.androidide.eventbus.events.file.FileCreationEvent -import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent -import com.itsaky.androidide.eventbus.events.file.FileRenameEvent -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.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import org.slf4j.LoggerFactory -import java.io.File - -/** - * Well-known registry key for the Kotlin source symbol index. - */ -val KOTLIN_SOURCE_SYMBOL_INDEX = IndexKey("kotlin-source-symbols") - -/** - * [IndexingService] that scans all Kotlin source files in the open project and - * maintains an in-memory [KotlinSourceSymbolIndex]. - */ -class KotlinSourceIndexingService( - private val context: Context, -) : IndexingService { - - companion object { - const val ID = "kotlin-source-indexing-service" - private val log = LoggerFactory.getLogger(KotlinSourceIndexingService::class.java) - } - - override val id = ID - override val providedKeys = listOf(KOTLIN_SOURCE_SYMBOL_INDEX) - - private var index: KotlinSourceSymbolIndex? = null - private val refreshMutex = Mutex() - private val coroutineScope = CoroutineScope(Dispatchers.Default) - - override suspend fun initialize(registry: IndexRegistry) { - val sourceIndex = KotlinSourceSymbolIndex.create(context) - this.index = sourceIndex - registry.register(KOTLIN_SOURCE_SYMBOL_INDEX, sourceIndex) - - if (!EventBus.getDefault().isRegistered(this)) { - EventBus.getDefault().register(this) - } - - log.info("Kotlin source symbol index initialized") - } - - override fun close() { - EventBus.getDefault().unregister(this) - coroutineScope.cancelIfActive("Kotlin source indexing service closed") - index?.close() - index = null - } - - /** - * Scans all `.kt` source files across all project modules and indexes any - * file not yet present in the in-memory index. - */ - fun refresh() { - coroutineScope.launch { - refreshMutex.withLock { indexAllSourceFiles() } - } - } - - private suspend fun indexAllSourceFiles() { - val index = this.index ?: run { - log.warn("Kotlin source index not initialized; skipping refresh") - return - } - - val workspace = ProjectManagerImpl.getInstance().workspace ?: run { - log.warn("Workspace model not available; skipping Kotlin source scan") - return - } - - val sourceFiles = workspace.subProjects - .asSequence() - .filterIsInstance() - .flatMap { module -> module.getSourceDirectories().asSequence() } - .filter { it.exists() && it.isDirectory } - .flatMap { dir -> dir.walkTopDown().filter { it.isFile && it.extension == "kt" } } - .map { it.absolutePath } - .toList() - - log.info("Found {} Kotlin source files to index", sourceFiles.size) - - var submitted = 0 - for (filePath in sourceFiles) { - if (!index.isFileCached(filePath)) { - submitted++ - index.indexFile(filePath) - } - } - - if (submitted > 0) { - log.info("{} Kotlin source files submitted for background indexing", submitted) - } else { - log.info("All Kotlin source files already cached, nothing to index") - } - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("UNUSED") - fun onFileCreated(event: FileCreationEvent) { - if (!event.file.isKotlinSource) return - val filePath = event.file.absolutePath - log.debug("File created, indexing: {}", filePath) - index?.indexFile(filePath) - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("UNUSED") - fun onFileSaved(event: DocumentSaveEvent) { - val filePath = event.savedFile.toAbsolutePath().toString() - if (!filePath.endsWith(".kt")) return - log.debug("File saved, re-indexing: {}", filePath) - index?.reindexFile(filePath) - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("UNUSED") - fun onFileDeleted(event: FileDeletionEvent) { - if (!event.file.isKotlinSource) return - val filePath = event.file.absolutePath - log.debug("File deleted, removing from index: {}", filePath) - index?.removeFile(filePath) - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("UNUSED") - fun onFileRenamed(event: FileRenameEvent) { - val oldPath = event.file.absolutePath - val newPath = event.newFile.absolutePath - - if (event.file.isKotlinSource) { - log.debug("File renamed, removing old path from index: {}", oldPath) - index?.removeFile(oldPath) - } - - if (event.newFile.isKotlinSource) { - log.debug("File renamed, indexing new path: {}", newPath) - index?.indexFile(newPath) - } - } - - private val File.isKotlinSource: Boolean - get() = isFile && extension == "kt" -} diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt deleted file mode 100644 index 3f5cb2725b..0000000000 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceScanner.kt +++ /dev/null @@ -1,469 +0,0 @@ -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.appdevforall.codeonthego.indexing.jvm.KotlinSourceScanner.fqnToInternalName -import org.appdevforall.codeonthego.indexing.jvm.KotlinSourceScanner.scan -import org.appdevforall.codeonthego.lsp.kotlin.parser.KotlinParser -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassKind -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.PropertySymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolBuilder -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeAliasSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeReference -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Visibility -import org.slf4j.LoggerFactory -import java.io.File - -/** - * Parses a Kotlin source file and produces [JvmSymbol] entries for indexing, - * working directly from the [SymbolBuilder] output. - * - * Type references coming from source (which are as-written, dot-separated) - * are converted to internal names via [fqnToInternalName], which applies the - * standard Java naming convention: lowercase segments are package components, - * uppercase-starting segments are class components. - * - * Thread safety: each call to [scan] creates its own [KotlinParser] instance. - */ -object KotlinSourceScanner { - - private val log = LoggerFactory.getLogger(KotlinSourceScanner::class.java) - - /** - * Parses the Kotlin source file at [filePath] and emits a [JvmSymbol] for - * each public/internal declaration found (classes and their members, - * top-level functions, properties, and type aliases). - */ - fun scan(filePath: String, sourceId: String): Flow = flow { - val file = File(filePath) - if (!file.exists() || !file.isFile) return@flow - - val content = try { - file.readText() - } catch (e: Exception) { - log.warn("Failed to read source file: {}", filePath, e) - return@flow - } - - KotlinParser().use { parser -> - val result = parser.parse(content, filePath) - result.tree.use { syntaxTree -> - val symbolTable = SymbolBuilder.build(syntaxTree, filePath) - // Internal prefix for this file's package: "com/example" - val pkgInternal = symbolTable.packageName.replace('.', '/') - - for (symbol in symbolTable.topLevelSymbols) { - for (jvmSymbol in toJvmSymbols( - symbol, - pkgInternal, - containingClass = "", - sourceId - )) { - emit(jvmSymbol) - } - } - } - } - }.flowOn(Dispatchers.IO) - - private fun toJvmSymbols( - symbol: Symbol, - pkgInternal: String, - containingClass: String, - sourceId: String, - ): List = when (symbol) { - is ClassSymbol -> classSymbols(symbol, pkgInternal, containingClass, sourceId) - is FunctionSymbol -> listOfNotNull( - functionSymbol( - symbol, - pkgInternal, - containingClass, - sourceId - ) - ) - - is PropertySymbol -> listOfNotNull( - propertySymbol( - symbol, - pkgInternal, - containingClass, - sourceId - ) - ) - - is TypeAliasSymbol -> listOfNotNull( - typeAliasSymbol( - symbol, - pkgInternal, - containingClass, - sourceId - ) - ) - - else -> emptyList() - } - - private fun classSymbols( - symbol: ClassSymbol, - pkgInternal: String, - containingClass: String, - sourceId: String, - ): List { - // Enum entries use a member-style key relative to their containing enum. - if (symbol.kind == ClassKind.ENUM_ENTRY) { - return listOf(enumEntrySymbol(symbol, pkgInternal, containingClass, sourceId)) - } - - val visibility = mapVisibility(symbol.visibility) - if (visibility == JvmVisibility.PRIVATE) return emptyList() - - // Internal name: "com/example/Outer$Inner" for nested, "com/example/Outer" for top-level. - val classInternalName = buildClassInternalName(symbol.name, pkgInternal, containingClass) - val packageName = pkgInternal.replace('/', '.') - - val kind = mapClassKind(symbol) - - val classSymbol = JvmSymbol( - key = classInternalName, - sourceId = sourceId, - name = classInternalName, - shortName = symbol.name, - packageName = packageName, - kind = kind, - language = JvmSourceLanguage.KOTLIN, - visibility = visibility, - data = JvmClassInfo( - internalName = classInternalName, - containingClassName = containingClass, - supertypeNames = symbol.superTypes.map { fqnToInternalName(it.name) }, - typeParameters = symbol.typeParameters.map { it.name }, - isAbstract = symbol.modifiers.isAbstract, - isFinal = symbol.modifiers.isFinal, - isInner = symbol.modifiers.isInner, - kotlin = KotlinClassInfo( - isData = symbol.modifiers.isData, - isValue = symbol.modifiers.isValue, - isSealed = symbol.modifiers.isSealed, - ), - ), - ) - - val result = mutableListOf(classSymbol) - - // Primary constructor (not always in the member scope, depending on SymbolBuilder). - symbol.primaryConstructor?.let { ctor -> - if (!ctor.isPrimaryConstructor || ctor !in (symbol.memberScope?.allSymbols - ?: emptyList()) - ) { - functionSymbol(ctor, pkgInternal, classInternalName, sourceId)?.let { result += it } - } - } - - // All members: nested classes, secondary constructors, functions, properties. - for (member in symbol.memberScope?.allSymbols ?: emptyList()) { - result += toJvmSymbols(member, pkgInternal, classInternalName, sourceId) - } - - return result - } - - private fun enumEntrySymbol( - symbol: ClassSymbol, - pkgInternal: String, - containingClass: String, - sourceId: String, - ): JvmSymbol { - val packageName = pkgInternal.replace('/', '.') - return JvmSymbol( - key = "$containingClass#${symbol.name}", - sourceId = sourceId, - name = "$containingClass#${symbol.name}", - shortName = symbol.name, - packageName = packageName, - kind = JvmSymbolKind.ENUM_ENTRY, - language = JvmSourceLanguage.KOTLIN, - data = JvmEnumEntryInfo(containingClassName = containingClass), - ) - } - - private fun functionSymbol( - symbol: FunctionSymbol, - pkgInternal: String, - containingClass: String, - sourceId: String, - ): JvmSymbol? { - val visibility = mapVisibility(symbol.visibility) - if (visibility == JvmVisibility.PRIVATE) return null - - val kind = when { - symbol.isConstructor -> JvmSymbolKind.CONSTRUCTOR - symbol.isExtension -> JvmSymbolKind.EXTENSION_FUNCTION - else -> JvmSymbolKind.FUNCTION - } - - val packageName = pkgInternal.replace('/', '.') - val owner = containingClass.ifEmpty { pkgInternal } - - // For constructors, the short name is the class's simple name. - val shortName = if (symbol.isConstructor) { - containingClass.substringAfterLast('/').substringAfterLast('$') - } else { - symbol.name - } - val name = "$owner#$shortName" - - val parameters = symbol.parameters.map { param -> - JvmParameterInfo( - name = param.name, - typeName = param.type?.let { fqnToInternalName(it.name) } ?: "", - typeDisplayName = param.type?.render() ?: "", - hasDefaultValue = param.hasDefaultValue, - isVararg = param.isVararg, - isCrossinline = param.isCrossinline, - isNoinline = param.isNoinline, - ) - } - - val returnTypeInternal = - symbol.returnType?.let { fqnToInternalName(it.name) } ?: "kotlin/Unit" - val returnTypeDisplay = symbol.returnType?.render() ?: "Unit" - val receiverType = symbol.receiverType - - val key = "$name(${parameters.joinToString(",") { it.typeName }})" - - val signatureDisplay = buildString { - receiverType?.let { append(displayName(it)).append('.') } - if (symbol.typeParameters.isNotEmpty()) { - append('<') - append(symbol.typeParameters.joinToString { it.name }) - append('>') - } - append('(') - append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplayName}" }) - append(')') - if (!symbol.isConstructor) { - append(": ") - append(returnTypeDisplay) - } - } - - return JvmSymbol( - key = key, - sourceId = sourceId, - name = name, - shortName = shortName, - packageName = packageName, - kind = kind, - language = JvmSourceLanguage.KOTLIN, - visibility = visibility, - data = JvmFunctionInfo( - containingClassName = containingClass, - returnTypeName = returnTypeInternal, - returnTypeDisplayName = returnTypeDisplay, - parameterCount = parameters.size, - parameters = parameters, - signatureDisplay = signatureDisplay, - typeParameters = symbol.typeParameters.map { it.name }, - isAbstract = symbol.modifiers.isAbstract, - isFinal = symbol.modifiers.isFinal, - kotlin = KotlinFunctionInfo( - receiverTypeName = receiverType?.let { fqnToInternalName(it.name) } ?: "", - receiverTypeDisplayName = receiverType?.let { displayName(it) } ?: "", - isSuspend = symbol.isSuspend, - isInline = symbol.isInline, - isInfix = symbol.isInfix, - isOperator = symbol.isOperator, - isTailrec = symbol.isTailrec, - isExternal = symbol.modifiers.isExternal, - isExpect = symbol.modifiers.isExpect, - isActual = symbol.modifiers.isActual, - isReturnTypeNullable = symbol.returnType?.isNullable ?: false, - ), - ), - ) - } - - private fun propertySymbol( - symbol: PropertySymbol, - pkgInternal: String, - containingClass: String, - sourceId: String, - ): JvmSymbol? { - val visibility = mapVisibility(symbol.visibility) - if (visibility == JvmVisibility.PRIVATE) return null - - val kind = - if (symbol.isExtension) JvmSymbolKind.EXTENSION_PROPERTY else JvmSymbolKind.PROPERTY - val packageName = pkgInternal.replace('/', '.') - val owner = containingClass.ifEmpty { pkgInternal } - val name = "$owner#${symbol.name}" - val receiverType = symbol.receiverType - - return JvmSymbol( - key = name, - sourceId = sourceId, - name = name, - shortName = symbol.name, - packageName = packageName, - kind = kind, - language = JvmSourceLanguage.KOTLIN, - visibility = visibility, - data = JvmFieldInfo( - containingClassName = containingClass, - typeName = symbol.type?.let { fqnToInternalName(it.name) } ?: "", - typeDisplayName = symbol.type?.let { displayName(it) } ?: "", - isFinal = !symbol.isVar, - kotlin = KotlinPropertyInfo( - receiverTypeName = receiverType?.let { fqnToInternalName(it.name) } ?: "", - receiverTypeDisplayName = receiverType?.let { displayName(it) } ?: "", - isConst = symbol.isConst, - isLateinit = symbol.isLateInit, - hasGetter = symbol.hasCustomGetter, - hasSetter = symbol.hasCustomSetter, - isDelegated = symbol.isDelegated, - isExpect = symbol.modifiers.isExpect, - isActual = symbol.modifiers.isActual, - isExternal = symbol.modifiers.isExternal, - isTypeNullable = symbol.type?.isNullable ?: false, - ), - ), - ) - } - - private fun typeAliasSymbol( - symbol: TypeAliasSymbol, - pkgInternal: String, - containingClass: String, - sourceId: String, - ): JvmSymbol? { - val visibility = mapVisibility(symbol.visibility) - if (visibility == JvmVisibility.PRIVATE) return null - - val packageName = pkgInternal.replace('/', '.') - val internalName = buildClassInternalName(symbol.name, pkgInternal, containingClass) - - return JvmSymbol( - key = internalName, - sourceId = sourceId, - name = internalName, - shortName = symbol.name, - packageName = packageName, - kind = JvmSymbolKind.TYPE_ALIAS, - language = JvmSourceLanguage.KOTLIN, - visibility = visibility, - data = JvmTypeAliasInfo( - containingClassName = containingClass, - expandedTypeName = symbol.underlyingType?.let { fqnToInternalName(it.name) } ?: "", - expandedTypeDisplayName = symbol.underlyingType?.let { displayName(it) } ?: "", - typeParameters = symbol.typeParameters.map { it.name }, - ), - ) - } - - /** - * Builds the JVM internal name for a class. - * - * - Top-level: `com/example/MyClass` - * - Nested: `com/example/MyClass$Inner` - */ - private fun buildClassInternalName( - simpleName: String, - pkgInternal: String, - containingClass: String, - ): String = when { - containingClass.isNotEmpty() -> $$"$$containingClass$$$simpleName" - pkgInternal.isNotEmpty() -> "$pkgInternal/$simpleName" - else -> simpleName - } - - /** - * Converts a dot-separated FQN (as written in Kotlin source) to a JVM internal name. - * - * Applies standard Java naming conventions: lowercase-starting segments are - * package components (joined with `/`), uppercase-starting segments are class - * components (joined with `$`). - * - * Examples: - * ``` - * "String" → "String" - * "kotlin.String" → "kotlin/String" - * "java.util.Map" → "java/util/Map" - * "java.util.Map.Entry" → "java/util/Map$Entry" - * ``` - */ - internal fun fqnToInternalName(fqn: String): String { - if (fqn.isEmpty() || !fqn.contains('.')) return fqn - - val parts = fqn.split('.') - val pkg = mutableListOf() - val cls = mutableListOf() - - for (part in parts) { - if (cls.isEmpty() && part.isNotEmpty() && part[0].isLowerCase()) { - pkg += part - } else { - cls += part - } - } - - return when { - pkg.isEmpty() -> cls.joinToString("$") - cls.isEmpty() -> pkg.joinToString("/") - else -> "${pkg.joinToString("/")}/${cls.joinToString("$")}" - } - } - - /** - * Returns a short display name for a [TypeReference]: the simple class name - * with type arguments rendered, but without the package prefix. - * - * E.g. `java.util.Map.Entry` → `Entry`, `List` → `List`. - */ - private fun displayName(ref: TypeReference): String { - val baseName = fqnToInternalName(ref.name) - .substringAfterLast('/') - .substringAfterLast('$') - val args = ref.typeArguments - return buildString { - append(baseName) - if (args.isNotEmpty()) { - append('<') - append(args.joinToString(", ") { displayName(it) }) - append('>') - } - if (ref.isNullable) append('?') - } - } - - private fun mapClassKind(symbol: ClassSymbol): JvmSymbolKind = when (symbol.kind) { - ClassKind.CLASS -> when { - symbol.modifiers.isSealed -> JvmSymbolKind.SEALED_CLASS - else -> JvmSymbolKind.CLASS - } - - ClassKind.INTERFACE -> when { - symbol.modifiers.isSealed -> JvmSymbolKind.SEALED_INTERFACE - else -> JvmSymbolKind.INTERFACE - } - - ClassKind.OBJECT -> JvmSymbolKind.OBJECT - ClassKind.COMPANION_OBJECT -> JvmSymbolKind.COMPANION_OBJECT - ClassKind.ENUM_CLASS -> JvmSymbolKind.ENUM - ClassKind.ENUM_ENTRY -> JvmSymbolKind.ENUM_ENTRY - ClassKind.ANNOTATION_CLASS -> JvmSymbolKind.ANNOTATION_CLASS - ClassKind.DATA_CLASS -> JvmSymbolKind.DATA_CLASS - ClassKind.VALUE_CLASS -> JvmSymbolKind.VALUE_CLASS - } - - private fun mapVisibility(visibility: Visibility): JvmVisibility = when (visibility) { - Visibility.PUBLIC -> JvmVisibility.PUBLIC - Visibility.PROTECTED -> JvmVisibility.PROTECTED - Visibility.INTERNAL -> JvmVisibility.INTERNAL - Visibility.PRIVATE -> JvmVisibility.PRIVATE - } -} diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceSymbolIndex.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceSymbolIndex.kt deleted file mode 100644 index f888fdfed5..0000000000 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinSourceSymbolIndex.kt +++ /dev/null @@ -1,173 +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.SQLiteIndex -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 extracted from Kotlin source files in the project. - * - * Unlike [JvmLibrarySymbolIndex], which accumulates a persistent on-disk cache - * of library JARs across IDE sessions, this index is deliberately **in-memory**: - * it is rebuilt from scratch on each project open and discarded when the project - * closes. This is correct because source files are cheap to re-parse (tree-sitter - * is fast) and the index must always reflect the current on-disk state. - */ -class KotlinSourceSymbolIndex private constructor( - val sourceIndex: SQLiteIndex, - val sourceIndexer: BackgroundIndexer, -) : Closeable { - - companion object { - - const val INDEX_NAME_SOURCE = "kotlin-source-index" - - /** - * Creates a [KotlinSourceSymbolIndex] 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 in this case. - */ - fun create(context: Context): KotlinSourceSymbolIndex { - // dbName = null → AndroidX SQLiteOpenHelper creates an in-memory database. - val index = SQLiteIndex( - descriptor = JvmSymbolDescriptor, - context = context, - dbName = null, - name = INDEX_NAME_SOURCE, - ) - val indexer = BackgroundIndexer(index) - return KotlinSourceSymbolIndex( - sourceIndex = index, - sourceIndexer = indexer, - ) - } - } - - /** - * Indexes the symbols in [filePath], skipping the file if it was already - * indexed in this session. - * - * Use [reindexFile] to force re-parsing (e.g. after a save event). - */ - fun indexFile( - filePath: String, - provider: (sourceId: String) -> Flow = { sourceId -> - KotlinSourceScanner.scan(filePath, sourceId) - }, - ) = sourceIndexer.indexSource(filePath, skipIfExists = true, provider) - - /** - * Re-indexes [filePath] unconditionally, removing any previously indexed - * symbols for that file first. - * - * Call this after the file is saved to disk. - */ - fun reindexFile( - filePath: String, - provider: (sourceId: String) -> Flow = { sourceId -> - KotlinSourceScanner.scan(filePath, sourceId) - }, - ) = sourceIndexer.indexSource(filePath, skipIfExists = false, provider) - - /** - * Removes all symbols that originate from [filePath] from the index. - * - * Implemented by scheduling an indexing job with an empty provider so that - * the [BackgroundIndexer] properly cancels any in-flight job for the same - * source before clearing the entries. - */ - fun removeFile(filePath: String) { - sourceIndexer.indexSource( - sourceId = filePath, - skipIfExists = false, - ) { kotlinx.coroutines.flow.emptyFlow() } - } - - /** - * Returns `true` if [filePath] has already been indexed in this session. - */ - suspend fun isFileCached(filePath: String): Boolean = - sourceIndex.containsSource(filePath) - - /** Prefix-based completion across all source symbols. */ - fun findByPrefix(prefix: String, limit: Int = 200): Flow = - sourceIndex.query(indexQuery { prefix(KEY_NAME, prefix); this.limit = limit }) - - /** Prefix-based completion filtered to specific [kinds]. */ - fun findByPrefix( - prefix: String, - kinds: Set, - limit: Int = 200, - ): Flow = - sourceIndex.query(indexQuery { prefix(KEY_NAME, prefix); this.limit = 0 }) - .filter { it.kind in kinds } - .take(limit) - - /** Find extension functions / properties declared for [receiverTypeFqName]. */ - fun findExtensionsFor( - receiverTypeFqName: String, - namePrefix: String = "", - limit: Int = 200, - ): Flow = sourceIndex.query(indexQuery { - eq(KEY_RECEIVER_TYPE, receiverTypeFqName) - if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) - this.limit = limit - }) - - /** Top-level callable symbols (functions, properties) in a package. */ - fun findTopLevelCallablesInPackage( - packageName: String, - namePrefix: String = "", - limit: Int = 200, - ): Flow = sourceIndex.query(indexQuery { - eq(KEY_PACKAGE, packageName) - if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) - this.limit = 0 - }).filter { it.kind.isCallable && it.isTopLevel }.take(limit) - - /** Top-level classifier symbols (classes, interfaces, objects…) in a package. */ - fun findClassifiersInPackage( - packageName: String, - namePrefix: String = "", - limit: Int = 200, - ): Flow = sourceIndex.query(indexQuery { - eq(KEY_PACKAGE, packageName) - if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) - this.limit = 0 - }).filter { it.kind.isClassifier }.take(limit) - - /** Members of a specific class (functions, properties). */ - fun findMembersOf( - classFqName: String, - namePrefix: String = "", - limit: Int = 200, - ): Flow = sourceIndex.query(indexQuery { - eq(KEY_CONTAINING_CLASS, classFqName) - if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) - this.limit = limit - }) - - /** Point lookup by fully-qualified name. */ - suspend fun findByFqName(fqName: String): JvmSymbol? = sourceIndex.get(fqName) - - /** All distinct package names present in the index. */ - fun allPackages(): Flow = sourceIndex.distinctValues(KEY_PACKAGE) - - /** Suspends until all in-flight indexing jobs complete. */ - suspend fun awaitIndexing() = sourceIndexer.awaitAll() - - override fun close() { - sourceIndexer.close() - sourceIndex.close() - } -} 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 394d7cdcaf..61dc5c6f56 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,8 +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.KotlinSourceIndexingService +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 @@ -80,6 +82,8 @@ class KotlinLanguageServer : ILanguageServer { private val scope = CoroutineScope(SupervisorJob() + CoroutineName(KotlinLanguageServer::class.simpleName!!)) private var projectModel: KotlinProjectModel? = null + private val sourceIndex: JvmSymbolIndex? = null + private val fileIndex: KtFileMetadataIndex? = null private var compiler: Compiler? = null private var analyzeJob: Job? = null @@ -105,10 +109,6 @@ class KotlinLanguageServer : ILanguageServer { if (!EventBus.getDefault().isRegistered(this)) { EventBus.getDefault().register(this) } - - ProjectManagerImpl.getInstance().indexingServiceManager.register( - KotlinSourceIndexingService(context = BaseApplication.baseInstance) - ) } override fun shutdown() { @@ -129,22 +129,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? - val kotlinSourceIndexingService = - indexingServiceManager.getService(KotlinSourceIndexingService.ID) as? KotlinSourceIndexingService? - jvmIndexingService?.refresh() - kotlinSourceIndexingService?.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 @@ -159,6 +173,7 @@ class KotlinLanguageServer : ILanguageServer { this.projectModel = model val compiler = Compiler( + workspace = workspace, projectModel = model, intellijPluginRoot = intellijPluginRoot, jdkHome = jdkHome, @@ -256,8 +271,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 @@ -292,8 +306,7 @@ class KotlinLanguageServer : ILanguageServer { } compiler?.compilationEnvironmentFor(event.changedFile)?.apply { - val content = FileManager.getDocumentContents(event.changedFile) - fileManager.onFileContentChanged(event.changedFile, content) + onFileContentChanged(event.changedFile) } debouncingAnalyze() @@ -307,8 +320,7 @@ class KotlinLanguageServer : ILanguageServer { } compiler?.compilationEnvironmentFor(event.closedFile)?.apply { - fileManager.onFileClosed(event.closedFile) - fileManager.clearAnalyzeTimestampOf(event.closedFile) + onFileClosed(event.closedFile) } if (FileManager.getActiveDocumentCount() == 0) { @@ -317,18 +329,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 b747556854..a8bb55edba 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,32 +1,72 @@ 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.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.appdevforall.codeonthego.indexing.jvm.KotlinSourceSymbolIndex -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.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.KotlinCoreApplicationEnvironmentMode +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.setupHighestLanguageLevel +import org.jetbrains.kotlin.cli.jvm.compiler.setupIdeaStandaloneExecution +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.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 @@ -38,6 +78,8 @@ 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 @@ -53,8 +95,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, @@ -63,41 +108,53 @@ internal class CompilationEnvironment( ) : KotlinProjectModel.ProjectModelListener, AutoCloseable { private var disposable = Disposer.newDisposable() - var session: StandaloneAnalysisAPISession - private set - - var parser: KtPsiFactory - private set - - var fileManager: KtFileManager - private set + val application: MockApplication + val project: MockProject + 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 libraryIndex: JvmSymbolIndex? + get() = ktProject.libraryIndex - val modificationTrackerFactory: KotlinModificationTrackerFactory - get() = session.project.getService(KotlinModificationTrackerFactory::class.java) + val requireLibraryIndex: JvmSymbolIndex + get() = checkNotNull(libraryIndex) - val coreApplicationEnvironment: CoreApplicationEnvironment - get() = session.coreApplicationEnvironment + val sourceIndex: JvmSymbolIndex? + get() = ktProject.sourceIndex - val symbolVisibilityChecker: SymbolVisibilityChecker? - get() = project.symbolVisibilityChecker + val requireSourceIndex: JvmSymbolIndex + get() = checkNotNull(sourceIndex) - val requireSymbolVisibilityChecker: SymbolVisibilityChecker - get() = checkNotNull(symbolVisibilityChecker) + val fileIndex: KtFileMetadataIndex? + get() = ktProject.fileIndex - val libraryIndex: JvmLibrarySymbolIndex? - get() = project.libraryIndex + val requireFileIndex: KtFileMetadataIndex + get() = checkNotNull(fileIndex) - val requireLibraryIndex: JvmLibrarySymbolIndex - get() = checkNotNull(libraryIndex) + val symbolVisibilityChecker: SymbolVisibilityChecker by lazy { + val provider = + project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider + SymbolVisibilityChecker(provider) + } - val sourceIndex: KotlinSourceSymbolIndex? - get() = project.sourceIndex + 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() { @@ -122,39 +179,152 @@ internal class CompilationEnvironment( } init { - session = buildSession() - parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) - fileManager = KtFileManager(parser, psiManager, psiDocumentManager) + System.setProperty("java.awt.headless", "true") + setupIdeaStandaloneExecution() - project.addListener(this) - } + val appEnv = KotlinCoreEnvironment.getOrCreateApplicationEnvironment( + projectDisposable = disposable, + configuration = createCompilerConfiguration(), + environmentMode = KotlinCoreApplicationEnvironmentMode.Production, + ) - private fun buildSession(): StandaloneAnalysisAPISession { - val configuration = createCompilerConfiguration() + val projectEnv = KotlinCoreProjectEnvironment( + disposable = disposable, + applicationEnvironment = appEnv + ) - val session = buildStandaloneAnalysisAPISession( - projectDisposable = disposable, - unitTestMode = false, - compilerConfiguration = configuration, + project = projectEnv.project + project.registerRWLock() + + application = appEnv.application + + 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, appEnv) + + project.setupHighestLanguageLevel() + val librariesScope = ProjectScope.getLibrariesScope(project) + val libraryRoots = modules + .asFlatSequence() + .filterNot { it.isSourceModule } + .flatMap { + it.computeFiles(extended = true).map { JavaRoot(it, 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) } + val rootsIndex = + JvmDependenciesDynamicCompoundIndex(shouldOnlyFindFirstClass = false).apply { + addIndex( + JvmDependenciesIndexImpl( + libraryRoots, + shouldOnlyFindFirstClass = false + ) + ) // TODO Should receive all (sources + libraries) + + 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) + } + } + } + } - return session - } + javaFileManager.initialize( + index = rootsIndex, + packagePartProviders = listOf(packagePartProvider), + singleJavaFileRootsIndex = SingleJavaFileRootsIndex(emptyList()), + usePsiClassFilesReading = true, + perfManager = null, + ) - private fun rebuildSession() { - logger.info("Rebuilding analysis session") + val fileFinderFactory = CliVirtualFileFinderFactory(rootsIndex, false, perfManager = null) - disposable.dispose() - disposable = Disposer.newDisposable() + 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 + ) + } - session = buildSession() - parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) + commandProcessor = application.getService(CommandProcessor::class.java) + parser = KtPsiFactory(project, eventSystemEnabled = enableParserEventSystem) - logger.info("Analysis session rebuilt") + // Sync the index in the background + ktSymbolIndex.syncIndexInBackground() } private fun createCompilerConfiguration(): CompilerConfiguration { @@ -176,81 +346,38 @@ 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 - ) - - registerService( - KotlinPackageProviderFactory::class.java, - KotlinStandalonePackageProviderFactory( - project, - sourceKtFiles + decProviderFactory.getAdditionalCreatedKtFiles() - ) - ) - } - - val modificationTrackerFactory = - project.getService(KotlinModificationTrackerFactory::class.java) as? KotlinStandaloneModificationTrackerFactory? - val sourceModificationTracker = - modificationTrackerFactory?.createProjectWideSourceModificationTracker() as? SimpleModificationTracker? - sourceModificationTracker?.incModificationCount() - } + fun onFileOpen(path: Path) { + val ktFile = loadKtFile(path) ?: return + ktSymbolIndex.openKtFile(path, ktFile) + } - logger.info("Refreshed: {} source KtFiles", sourceKtFiles.size) + fun onFileClosed(path: Path) { + ktSymbolIndex.closeKtFile(path) } - @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) - } + fun onFileContentChanged(path: Path) { + val ktFile = ktSymbolIndex.getOpenedKtFile(path) ?: return + val doc = project.read { psiDocumentManager.getDocument(ktFile) } ?: return + project.write { + commandProcessor.executeCommand(project, { + doc.setText(FileManager.getDocumentContents(path)) + psiDocumentManager.commitDocument(doc) + ktFile.onContentReload() + }, "onChangeFile", null) + + KaSourceModificationService.getInstance(project) + .handleElementModification(ktFile, 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() } @@ -258,9 +385,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 4069873445..04b6797d0c 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,25 +1,15 @@ 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_LIBRARY_SYMBOL_INDEX -import org.appdevforall.codeonthego.indexing.jvm.JvmLibrarySymbolIndex -import org.appdevforall.codeonthego.indexing.jvm.KOTLIN_SOURCE_SYMBOL_INDEX -import org.appdevforall.codeonthego.indexing.jvm.KotlinSourceSymbolIndex -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]. @@ -38,28 +28,28 @@ 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: KotlinSourceSymbolIndex? - get() = ProjectManagerImpl.getInstance() + val sourceIndex: JvmSymbolIndex? + get() = ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(KT_SOURCE_FILE_INDEX_KEY) + + val fileIndex: KtFileMetadataIndex? + get() = ProjectManagerImpl + .getInstance() .indexingServiceManager .registry - .get(KOTLIN_SOURCE_SYMBOL_INDEX) + .get(KT_SOURCE_FILE_META_INDEX_KEY) /** * The kind of change that occurred. @@ -106,102 +96,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() - val sourceRootToModuleMap = 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 - sourceRoots.forEach { root -> sourceRootToModuleMap[root] = module } - return module - } - - moduleProjects.forEach { addModule(getOrCreateModule(it)) } - - val moduleResolver = ModuleResolver( - jarMap = jarToModMap, - sourceRootMap = sourceRootToModuleMap, - ) - _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 d1372a0852..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ModuleResolver.kt +++ /dev/null @@ -1,36 +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.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule -import org.slf4j.LoggerFactory -import java.nio.file.Path -import java.nio.file.Paths - -internal class ModuleResolver( - private val jarMap: Map, - private val sourceRootMap: Map = emptyMap(), -) { - companion object { - private val logger = LoggerFactory.getLogger(ModuleResolver::class.java) - } - - /** - * Find the module that declares the given source ID. - * - * - For library JARs, the source ID is the JAR path — looked up directly. - * - For source files, the source ID is the `.kt` file path — resolved by - * finding the source root directory that is an ancestor of that path. - */ - fun findDeclaringModule(sourceId: String): KaModule? { - val path = Paths.get(sourceId) - jarMap[path]?.let { return it } - - // Walk source roots to find which module owns this file. - for ((root, module) in sourceRootMap) { - if (path.startsWith(root)) return module - } - - 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..3ac737e182 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt @@ -0,0 +1,14 @@ +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 + data class IndexLibraryFile(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..31ae41ad33 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt @@ -0,0 +1,120 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import com.itsaky.androidide.lsp.kotlin.compiler.read +import org.appdevforall.codeonthego.indexing.api.Index +import org.appdevforall.codeonthego.indexing.jvm.CombinedJarScanner +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol +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, + private val libraryIndex: JvmSymbolIndex, +) { + companion object { + private val logger = LoggerFactory.getLogger(IndexWorker::class.java) + } + + suspend fun start() { + var scanCount = 0 + var sourceIndexCount = 0 + var libraryIndexCount = 0 + + while (true) { + when (val command = queue.take()) { + is IndexCommand.IndexLibraryFile -> { + if (command.vf.fileSystem.protocol != "file") { + logger.warn("Unknown library file protocol: {}", command.vf.path) + continue + } + + if (command.vf.extension != "jar") { + logger.warn("Cannot index {} JVM library", command.vf.path) + continue + } + + libraryIndex.insertAll(CombinedJarScanner.scan(jarPath = command.vf.toNioPath())) + libraryIndexCount++ + } + + 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={}, libraryIndexCount={}", + scanCount, + sourceIndexCount, + libraryIndexCount + ) + } + + 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..0c20061acb --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt @@ -0,0 +1,140 @@ +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.openapi.vfs.VirtualFileManager +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, + libraryIndex = libraryIndex, + ) + + private val scanningWorker = ScanningWorker( + 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 = project.read { + PsiManager.getInstance(project) + .findFile(vf) as KtFile + } + + ktFileCache.put(path, ktFile) + return 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..938631499f --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt @@ -0,0 +1,57 @@ +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 java.util.concurrent.atomic.AtomicBoolean + +internal class ScanningWorker( + private val indexWorker: IndexWorker, + private val modules: List, +) { + + private val isRunning = AtomicBoolean(false) + + suspend fun start() { + isRunning.set(true) + try { + scan() + } finally { + isRunning.set(false) + } + } + + private suspend fun scan() { + val allModules = modules.asFlatSequence() + val sourceFiles = allModules + .filter { it.isSourceModule } + .flatMap { it.computeFiles(extended = true) } + .takeWhile { isRunning.get() } + .toList() + + 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)) + } + + allModules + .filterNot { it.isSourceModule } + .flatMap { it.computeFiles(extended = false) } + .takeWhile { isRunning.get() } + .forEach { indexWorker.submitCommand(IndexCommand.IndexLibraryFile(it)) } + + 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/KtLibraryModule.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt new file mode 100644 index 0000000000..0e553cae32 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt @@ -0,0 +1,133 @@ +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.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.config.JvmTarget +import org.jetbrains.kotlin.library.KLIB_FILE_EXTENSION +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +@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) } + } + + 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..8d1d062955 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspServiceRegistrar.kt @@ -0,0 +1,122 @@ +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.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.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.cli.jvm.compiler.MockExternalAnnotationsManager +import org.jetbrains.kotlin.cli.jvm.compiler.MockInferredAnnotationsManager +import org.jetbrains.kotlin.com.intellij.codeInsight.ExternalAnnotationsManager +import org.jetbrains.kotlin.com.intellij.codeInsight.InferredAnnotationsManager +import org.jetbrains.kotlin.com.intellij.core.CoreJavaFileManager +import org.jetbrains.kotlin.com.intellij.mock.MockApplication +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.extensions.DefaultPluginDescriptor +import org.jetbrains.kotlin.com.intellij.psi.SmartPointerManager +import org.jetbrains.kotlin.com.intellij.psi.SmartTypePointerManager +import org.jetbrains.kotlin.com.intellij.psi.impl.file.impl.JavaFileManager +import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl +import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartTypePointerManagerImpl + +@OptIn(KaImplementationDetail::class) +internal object LspServiceRegistrar : AnalysisApiSimpleServiceRegistrar() { + + private const val PLUGIN_RELATIVE_PATH = "/META-INF/kt-lsp/kt-lsp.xml" + private val pluginDescriptor = DefaultPluginDescriptor("kt-lsp-plugin-descriptor") + + 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) + } + } + + override fun registerProjectServices(project: MockProject) { + PluginStructureProvider.registerProjectServices(project, PLUGIN_RELATIVE_PATH) + + + with(project) { + registerService( + CoreJavaFileManager::class.java, + project.getService(JavaFileManager::class.java) as CoreJavaFileManager + ) + registerService(ExternalAnnotationsManager::class.java, MockExternalAnnotationsManager()) + registerService(InferredAnnotationsManager::class.java, MockInferredAnnotationsManager()) + 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 + ) + } + } +} 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/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..3f09e6253b --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt @@ -0,0 +1,113 @@ +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 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.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 java.nio.file.Paths + +internal class ProjectStructureProvider : KtLspService, KotlinProjectStructureProviderBase() { + + private lateinit var modules: List + private lateinit var project: Project + + 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 + val visited = mutableSetOf() + + modules.forEach { module -> + val foundModule = searchVirtualFileInModule(virtualFile, useSiteModule ?: module, visited) + if (foundModule != null) return foundModule + } + + 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 dd564eb93b..113efbbaf7 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 @@ -22,7 +22,6 @@ import org.appdevforall.codeonthego.indexing.jvm.JvmFunctionInfo import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolKind import org.appdevforall.codeonthego.indexing.jvm.JvmTypeAliasInfo -import org.appdevforall.codeonthego.indexing.jvm.JvmVisibility import org.jetbrains.kotlin.analysis.api.KaContextParameterApi import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.KaIdeApi @@ -61,6 +60,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" @@ -74,9 +74,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 } @@ -96,9 +96,9 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi } val completionKtFile = - managedFile.createInMemoryFileWithContent( - psiFactory = parser, - content = textWithPlaceholder + parser.createFile( + fileName = params.file.name, + text = textWithPlaceholder ) return try { @@ -282,26 +282,21 @@ private suspend fun KaSession.collectUnimportedSymbols( // 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) - ?.collect { symbol -> + ?.forEach { symbol -> val isVisible = visibilityChecker.isVisible( symbol = symbol, useSiteModule = useSiteModule, useSitePackage = currentPackage, ) - if (!isVisible) return@collect + if (!isVisible) return@forEach buildUnimportedSymbolItem(symbol)?.let { to += it } } // Source symbols: project .kt files — skip private and same-package symbols env.sourceIndex?.findByPrefix(ctx.partial) - ?.collect { symbol -> - if (symbol.packageName == currentPackage) return@collect + ?.forEach { symbol -> + if (symbol.packageName == currentPackage) return@forEach val isVisible = visibilityChecker.isVisible( symbol = symbol, @@ -309,7 +304,7 @@ private suspend fun KaSession.collectUnimportedSymbols( useSitePackage = currentPackage ) - if (!isVisible) return@collect + if (!isVisible) return@forEach buildUnimportedSymbolItem(symbol)?.let { to += it } } 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..afca38be9f 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,32 @@ 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) + logger.debug("Analyzing ktFile: {}", ktFile.text) + + 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..db61fb038f 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,6 +1,6 @@ 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 @@ -8,7 +8,7 @@ import org.jetbrains.kotlin.analysis.api.projectStructure.allDirectDependencies import java.util.concurrent.ConcurrentHashMap internal class SymbolVisibilityChecker( - private val moduleResolver: ModuleResolver, + private val structureProvider: ProjectStructureProvider, ) { // visibility check cache, for memoization // useSiteModule -> list of modules visible from useSiteModule @@ -19,7 +19,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/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/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( From a33427d080ee2bd3b5c1a1ee848cab401d88294f Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Apr 2026 01:17:01 +0530 Subject: [PATCH 40/58] fix: use StandaloneProjectFactory to create MockProject The StandaloneProjectFactory takes care of setting up a special MockProject instance which allows us to use Intellij's MessageBus to notify the analysis API about file changes. Signed-off-by: Akash Yadav --- .../kotlin/compiler/CompilationEnvironment.kt | 34 +++++++++++-------- .../compiler/registrar/LspServiceRegistrar.kt | 14 -------- 2 files changed, 19 insertions(+), 29 deletions(-) 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 a8bb55edba..d632317b72 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 @@ -29,6 +29,7 @@ import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProvider 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 @@ -40,6 +41,7 @@ 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 @@ -108,8 +110,17 @@ internal class CompilationEnvironment( ) : KotlinProjectModel.ProjectModelListener, AutoCloseable { private var disposable = Disposer.newDisposable() + val projectEnv: KotlinCoreProjectEnvironment + + val applicationEnv: KotlinCoreApplicationEnvironment + get() = projectEnv.environment as KotlinCoreApplicationEnvironment + val application: MockApplication + get() = applicationEnv.application + val project: MockProject + get() = projectEnv.project + val parser: KtPsiFactory val commandProcessor: CommandProcessor val modules: List @@ -182,22 +193,15 @@ internal class CompilationEnvironment( System.setProperty("java.awt.headless", "true") setupIdeaStandaloneExecution() - val appEnv = KotlinCoreEnvironment.getOrCreateApplicationEnvironment( - projectDisposable = disposable, - configuration = createCompilerConfiguration(), - environmentMode = KotlinCoreApplicationEnvironmentMode.Production, - ) - - val projectEnv = KotlinCoreProjectEnvironment( - disposable = disposable, - applicationEnvironment = appEnv - ) + projectEnv = StandaloneProjectFactory + .createProjectEnvironment( + projectDisposable = disposable, + applicationEnvironmentMode = KotlinCoreApplicationEnvironmentMode.Production, + compilerConfiguration = createCompilerConfiguration(), + ) - project = projectEnv.project project.registerRWLock() - application = appEnv.application - ApplicationServiceRegistration.registerWithCustomRegistration( application, serviceRegistrars, @@ -226,9 +230,8 @@ internal class CompilationEnvironment( serviceRegistrars.registerProjectServices(project, data = Unit) serviceRegistrars.registerProjectModelServices(project, disposable, data = Unit) - modules = workspace.collectKtModules(project, appEnv) + modules = workspace.collectKtModules(project, applicationEnv) - project.setupHighestLanguageLevel() val librariesScope = ProjectScope.getLibrariesScope(project) val libraryRoots = modules .asFlatSequence() @@ -253,6 +256,7 @@ internal class CompilationEnvironment( ).apply { addRoots(libraryRoots, MessageCollector.NONE) } + val rootsIndex = JvmDependenciesDynamicCompoundIndex(shouldOnlyFindFirstClass = false).apply { addIndex( 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 index 8d1d062955..b2ea451387 100644 --- 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 @@ -30,17 +30,10 @@ import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.Plugin 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.cli.jvm.compiler.MockExternalAnnotationsManager -import org.jetbrains.kotlin.cli.jvm.compiler.MockInferredAnnotationsManager -import org.jetbrains.kotlin.com.intellij.codeInsight.ExternalAnnotationsManager -import org.jetbrains.kotlin.com.intellij.codeInsight.InferredAnnotationsManager -import org.jetbrains.kotlin.com.intellij.core.CoreJavaFileManager import org.jetbrains.kotlin.com.intellij.mock.MockApplication import org.jetbrains.kotlin.com.intellij.mock.MockProject -import org.jetbrains.kotlin.com.intellij.openapi.extensions.DefaultPluginDescriptor import org.jetbrains.kotlin.com.intellij.psi.SmartPointerManager import org.jetbrains.kotlin.com.intellij.psi.SmartTypePointerManager -import org.jetbrains.kotlin.com.intellij.psi.impl.file.impl.JavaFileManager import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartTypePointerManagerImpl @@ -48,7 +41,6 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartTypePointer internal object LspServiceRegistrar : AnalysisApiSimpleServiceRegistrar() { private const val PLUGIN_RELATIVE_PATH = "/META-INF/kt-lsp/kt-lsp.xml" - private val pluginDescriptor = DefaultPluginDescriptor("kt-lsp-plugin-descriptor") override fun registerApplicationServices(application: MockApplication) { PluginStructureProvider.registerApplicationServices(application, PLUGIN_RELATIVE_PATH) @@ -68,12 +60,6 @@ internal object LspServiceRegistrar : AnalysisApiSimpleServiceRegistrar() { with(project) { - registerService( - CoreJavaFileManager::class.java, - project.getService(JavaFileManager::class.java) as CoreJavaFileManager - ) - registerService(ExternalAnnotationsManager::class.java, MockExternalAnnotationsManager()) - registerService(InferredAnnotationsManager::class.java, MockInferredAnnotationsManager()) registerService( KotlinLifetimeTokenFactory::class.java, KotlinReadActionConfinementLifetimeTokenFactory::class.java From 51d068640f6323bce696e756f4ee085fb68b196b Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Apr 2026 01:53:28 +0530 Subject: [PATCH 41/58] feat: add support for indexing classes using VirtualFile Signed-off-by: Akash Yadav --- .../indexing/jvm/CombinedJarScanner.kt | 23 + .../indexing/jvm/JarSymbolScanner.kt | 513 +++++++++--------- .../indexing/jvm/KotlinMetadataScanner.kt | 21 + .../lsp/kotlin/compiler/index/IndexWorker.kt | 15 +- .../kotlin/compiler/index/ScanningWorker.kt | 9 +- .../compiler/modules/KtLibraryModule.kt | 22 + 6 files changed, 345 insertions(+), 258 deletions(-) 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 64af7ce982..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,5 +1,8 @@ package org.appdevforall.codeonthego.indexing.jvm +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 @@ -18,6 +21,26 @@ object CombinedJarScanner { private val log = LoggerFactory.getLogger(CombinedJarScanner::class.java) + @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()) 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/KotlinMetadataScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt index a0c6580600..a230093803 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()) @@ -393,6 +413,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/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 index 31ae41ad33..629f483dce 100644 --- 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 @@ -1,9 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler.index import com.itsaky.androidide.lsp.kotlin.compiler.read -import org.appdevforall.codeonthego.indexing.api.Index import org.appdevforall.codeonthego.indexing.jvm.CombinedJarScanner -import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadata import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex @@ -31,17 +29,8 @@ internal class IndexWorker( while (true) { when (val command = queue.take()) { is IndexCommand.IndexLibraryFile -> { - if (command.vf.fileSystem.protocol != "file") { - logger.warn("Unknown library file protocol: {}", command.vf.path) - continue - } - - if (command.vf.extension != "jar") { - logger.warn("Cannot index {} JVM library", command.vf.path) - continue - } - - libraryIndex.insertAll(CombinedJarScanner.scan(jarPath = command.vf.toNioPath())) + logger.debug("index library: {}", command.vf.path) + libraryIndex.insertAll(CombinedJarScanner.scan(rootVf = command.vf)) libraryIndexCount++ } 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 index 938631499f..5cdc16cbc5 100644 --- 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 @@ -3,6 +3,7 @@ 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.slf4j.LoggerFactory import java.util.concurrent.atomic.AtomicBoolean internal class ScanningWorker( @@ -10,6 +11,10 @@ internal class ScanningWorker( private val modules: List, ) { + companion object { + private val logger = LoggerFactory.getLogger(ScanningWorker::class.java) + } + private val isRunning = AtomicBoolean(false) suspend fun start() { @@ -22,8 +27,9 @@ internal class ScanningWorker( } private suspend fun scan() { - val allModules = modules.asFlatSequence() + val allModules = modules.asFlatSequence().toList() val sourceFiles = allModules + .asSequence() .filter { it.isSourceModule } .flatMap { it.computeFiles(extended = true) } .takeWhile { isRunning.get() } @@ -43,6 +49,7 @@ internal class ScanningWorker( } allModules + .asSequence() .filterNot { it.isSourceModule } .flatMap { it.computeFiles(extended = false) } .takeWhile { isRunning.get() } 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 index 0e553cae32..ba2a273f92 100644 --- 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 @@ -10,17 +10,22 @@ 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, @@ -88,6 +93,23 @@ internal class KtLibraryModule( .flatMap { LibraryUtils.getAllVirtualFilesFromRoot(it, includeRoot = true) } } + override val baseContentScope: GlobalSearchScope by lazy { + val virtualFileUrls = computeFiles(extended = true).map { it.url }.toSet() + object : GlobalSearchScope(project) { + override fun contains(p0: VirtualFile): Boolean { + return p0.url in virtualFileUrls + } + + override fun isSearchInModuleContent(p0: Module): Boolean { + return false + } + + override fun isSearchInLibraries(): Boolean { + return true + } + } + } + override val libraryName: String get() = id From e77df4897662a6636a014b13665243f67c9363f0 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Apr 2026 19:37:23 +0530 Subject: [PATCH 42/58] feat: create in-memory KtFile instances for modified files Signed-off-by: Akash Yadav --- .../itsaky/androidide/app/IDEApplication.kt | 4 + .../service/IndexingServiceManager.kt | 5 - .../lsp/kotlin/KotlinLanguageServer.kt | 3 - .../kotlin/compiler/CompilationEnvironment.kt | 26 +-- .../lsp/kotlin/compiler/index/IndexWorker.kt | 1 - .../kotlin/compiler/index/KtSymbolIndex.kt | 11 +- .../compiler/modules/KtLibraryModule.kt | 7 +- .../compiler/registrar/LspServiceRegistrar.kt | 22 +++ .../services/NoOpAsyncExecutionService.kt | 157 ++++++++++++++++++ .../services/ProjectStructureProvider.kt | 27 +++ .../kotlin/completion/KotlinCompletions.kt | 86 +++++----- .../diagnostic/KotlinDiagnosticProvider.kt | 2 - .../kotlin-analysis-api/build.gradle.kts | 4 +- 13 files changed, 282 insertions(+), 73 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt 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/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..724804e7a5 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 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 61dc5c6f56..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 @@ -82,8 +82,6 @@ class KotlinLanguageServer : ILanguageServer { private val scope = CoroutineScope(SupervisorJob() + CoroutineName(KotlinLanguageServer::class.simpleName!!)) private var projectModel: KotlinProjectModel? = null - private val sourceIndex: JvmSymbolIndex? = null - private val fileIndex: KtFileMetadataIndex? = null private var compiler: Compiler? = null private var analyzeJob: Job? = null @@ -96,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" 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 d632317b72..8eefb9bbb8 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 @@ -190,8 +190,6 @@ internal class CompilationEnvironment( } init { - System.setProperty("java.awt.headless", "true") - setupIdeaStandaloneExecution() projectEnv = StandaloneProjectFactory .createProjectEnvironment( @@ -236,8 +234,8 @@ internal class CompilationEnvironment( val libraryRoots = modules .asFlatSequence() .filterNot { it.isSourceModule } - .flatMap { - it.computeFiles(extended = true).map { JavaRoot(it, JavaRoot.RootType.BINARY) } + .flatMap { libMod -> + libMod.computeFiles(extended = false).map { file -> JavaRoot(file, JavaRoot.RootType.BINARY) } } .toList() @@ -357,20 +355,22 @@ internal class CompilationEnvironment( fun onFileClosed(path: Path) { ktSymbolIndex.closeKtFile(path) + (project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider) + .unregisterInMemoryFile(path.pathString) } fun onFileContentChanged(path: Path) { - val ktFile = ktSymbolIndex.getOpenedKtFile(path) ?: return - val doc = project.read { psiDocumentManager.getDocument(ktFile) } ?: return - project.write { - commandProcessor.executeCommand(project, { - doc.setText(FileManager.getDocumentContents(path)) - psiDocumentManager.commitDocument(doc) - ktFile.onContentReload() - }, "onChangeFile", null) + val newContent = FileManager.getDocumentContents(path) + val newKtFile = project.read { parser.createFile(path.pathString, newContent) } + + // Tell ProjectStructureProvider which module owns this LightVirtualFile. + val provider = project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider + provider.registerInMemoryFile(path.pathString, newKtFile.virtualFile) + ktSymbolIndex.openKtFile(path, newKtFile) + project.write { KaSourceModificationService.getInstance(project) - .handleElementModification(ktFile, KaElementModificationType.Unknown) + .handleElementModification(newKtFile, KaElementModificationType.Unknown) } } 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 index 629f483dce..bd91a7109e 100644 --- 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 @@ -29,7 +29,6 @@ internal class IndexWorker( while (true) { when (val command = queue.take()) { is IndexCommand.IndexLibraryFile -> { - logger.debug("index library: {}", command.vf.path) libraryIndex.insertAll(CombinedJarScanner.scan(rootVf = command.vf)) libraryIndexCount++ } 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 index 0c20061acb..2a388b9447 100644 --- 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 @@ -15,7 +15,6 @@ 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.openapi.vfs.VirtualFileManager import org.jetbrains.kotlin.com.intellij.psi.PsiManager import org.jetbrains.kotlin.psi.KtFile import java.nio.file.Path @@ -112,15 +111,17 @@ internal class KtSymbolIndex( openedFiles[path]?.also { return it } ktFileCache.getIfPresent(path)?.also { return it } - val ktFile = project.read { - PsiManager.getInstance(project) - .findFile(vf) as KtFile - } + 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) 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 index ba2a273f92..85b7de142c 100644 --- 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 @@ -93,14 +93,15 @@ internal class KtLibraryModule( .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(p0: VirtualFile): Boolean { - return p0.url in virtualFileUrls + override fun contains(vf: VirtualFile): Boolean { + return vf.url in virtualFileUrls } - override fun isSearchInModuleContent(p0: Module): Boolean { + override fun isSearchInModuleContent(module: Module): Boolean { return false } 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 index b2ea451387..f3b0edbfa3 100644 --- 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 @@ -2,6 +2,7 @@ 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 @@ -11,6 +12,7 @@ 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 @@ -30,12 +32,22 @@ import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.Plugin 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() { @@ -52,6 +64,7 @@ internal object LspServiceRegistrar : AnalysisApiSimpleServiceRegistrar() { AnalysisPermissionOptions::class.java ) registerService(ClsKotlinBinaryClassCache::class.java) + registerService(AsyncExecutionService::class.java, NoOpAsyncExecutionService::class.java) } } @@ -105,4 +118,13 @@ internal object LspServiceRegistrar : AnalysisApiSimpleServiceRegistrar() { ) } } + + @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/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/ProjectStructureProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt index 3f09e6253b..7e63ee2494 100644 --- 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 @@ -13,13 +13,34 @@ 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.slf4j.LoggerFactory import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap 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", @@ -43,6 +64,9 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr useSiteModule: KaModule? ): KaModule { val virtualFile = element.containingFile.virtualFile + + inMemoryVfToModule[virtualFile]?.let { return it } + val visited = mutableSetOf() modules.forEach { module -> @@ -50,6 +74,9 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr if (foundModule != null) return foundModule } + // fallback: path-based lookup + findModuleForSourceId(virtualFile.path)?.let { return it } + return NotUnderContentRootModule( id = "unnamed-outside-content-root", moduleDescription = "unnamed-outside-content-root module with a PSI file.", 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 113efbbaf7..c76d3d5b0f 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 @@ -95,49 +97,54 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi append(originalText, completionOffset, originalText.length) } - val completionKtFile = + val completionKtFile = project.read { parser.createFile( fileName = params.file.name, text = textWithPlaceholder - ) + ).apply { + originalFile = 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 { - val items = mutableListOf() - val completionContext = determineCompletionContext(ctx.psiElement) - when (completionContext) { - CompletionContext.Scope -> - collectScopeCompletions(to = items) + context(ctx) { + runBlocking { + val items = mutableListOf() + val completionContext = determineCompletionContext(ctx.psiElement) + when (completionContext) { + CompletionContext.Scope -> + collectScopeCompletions(to = items) - CompletionContext.Member -> - collectMemberCompletions(to = items) - } + CompletionContext.Member -> + collectMemberCompletions(to = items) + } - collectKeywordCompletions(to = items) - CompletionResult(items) + collectKeywordCompletions(to = items) + CompletionResult(items) + } } } } @@ -146,7 +153,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 } } @@ -324,9 +331,10 @@ private fun KaSession.buildUnimportedSymbolItem(symbol: JvmSymbol): CompletionIt val receiverClassId = internalNameToClassId(receiverTypeName) val receiverType = findClass(receiverClassId) if (receiverType != null) { - val satisfiesImplicitReceivers = ctx.scopeContext.implicitReceivers.any { receiver -> - receiver.type.isSubtypeOf(receiverType) - } + 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 null 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 afca38be9f..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 @@ -43,8 +43,6 @@ private fun CompilationEnvironment.doAnalyze(file: Path): DiagnosticResult { return DiagnosticResult.NO_UPDATE } - logger.debug("Analyzing ktFile: {}", ktFile.text) - val diagnostics = project.read { analyze(ktFile) { ktFile.collectDiagnostics(KaDiagnosticCheckerFilter.EXTENDED_AND_COMMON_CHECKERS) 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", ) } } From 340b08b73965e2a15572a8be8f32dbcd1bfd1aae Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Apr 2026 20:39:21 +0530 Subject: [PATCH 43/58] fix: IndexWorker always re-index libraries Signed-off-by: Akash Yadav --- .../codeonthego/indexing/jvm/KotlinMetadataScanner.kt | 3 ++- .../androidide/lsp/kotlin/compiler/index/IndexWorker.kt | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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 a230093803..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 @@ -129,7 +129,8 @@ object KotlinMetadataScanner { } private fun extractFromClass( - klass: KmClass, sourceId: String, + klass: KmClass, + sourceId: String, ): List { val symbols = mutableListOf() val className = klass.name 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 index bd91a7109e..266f80f3d4 100644 --- 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 @@ -29,7 +29,7 @@ internal class IndexWorker( while (true) { when (val command = queue.take()) { is IndexCommand.IndexLibraryFile -> { - libraryIndex.insertAll(CombinedJarScanner.scan(rootVf = command.vf)) + libraryIndex.indexSource(command.vf.path) { CombinedJarScanner.scan(rootVf = command.vf) } libraryIndexCount++ } @@ -68,7 +68,9 @@ internal class IndexWorker( } is IndexCommand.ScanSourceFile -> { - val ktFile = project.read { PsiManager.getInstance(project).findFile(command.vf) as? KtFile } + val ktFile = project.read { + PsiManager.getInstance(project).findFile(command.vf) as? KtFile + } ?: continue val newFile = ktFile.toMetadata(project, isIndexed = false) From a1d999ef2b209d08db7b652952f0c1afd1f16161 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Apr 2026 22:22:39 +0530 Subject: [PATCH 44/58] feat: add KtFile.backingFilePath for better resolution of declaring modules Signed-off-by: Akash Yadav --- .../codeonthego/indexing/FilteredIndex.kt | 3 +- .../kotlin/compiler/CompilationEnvironment.kt | 4 +- .../lsp/kotlin/compiler/index/IndexWorker.kt | 9 ++++- .../kotlin/compiler/index/KtSymbolIndex.kt | 1 + .../kotlin/compiler/index/ScanningWorker.kt | 4 ++ .../lsp/kotlin/compiler/modules/KtFileExts.kt | 9 +++++ .../services/ProjectStructureProvider.kt | 40 ++++++++++++++++--- .../kotlin/completion/KotlinCompletions.kt | 33 ++++++++------- .../kotlin/utils/SymbolVisibilityChecker.kt | 5 +++ .../lsp/kotlin/utils/VirtualFileExts.kt | 7 ++++ 10 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt 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 1aced73afb..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 @@ -79,7 +79,8 @@ open class FilteredIndex( if (query.sourceId != null && query.sourceId !in activeSources) { 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? { 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 8eefb9bbb8..2389525252 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 @@ -3,6 +3,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler 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 @@ -45,8 +46,6 @@ 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.compiler.setupHighestLanguageLevel -import org.jetbrains.kotlin.cli.jvm.compiler.setupIdeaStandaloneExecution import org.jetbrains.kotlin.cli.jvm.index.JavaRoot import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesDynamicCompoundIndex import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndexImpl @@ -362,6 +361,7 @@ internal class CompilationEnvironment( fun onFileContentChanged(path: Path) { val newContent = FileManager.getDocumentContents(path) val newKtFile = project.read { parser.createFile(path.pathString, newContent) } + newKtFile.backingFilePath = path // Tell ProjectStructureProvider which module owns this LightVirtualFile. val provider = project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider 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 index 266f80f3d4..3195eb17c6 100644 --- 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 @@ -1,14 +1,17 @@ package com.itsaky.androidide.lsp.kotlin.compiler.index import com.itsaky.androidide.lsp.kotlin.compiler.read +import com.itsaky.androidide.lsp.kotlin.utils.toNioPathOrNull import org.appdevforall.codeonthego.indexing.jvm.CombinedJarScanner 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.com.intellij.util.io.URLUtil.JAR_SEPARATOR import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory +import kotlin.io.path.pathString internal class IndexWorker( private val project: Project, @@ -29,7 +32,11 @@ internal class IndexWorker( while (true) { when (val command = queue.take()) { is IndexCommand.IndexLibraryFile -> { - libraryIndex.indexSource(command.vf.path) { CombinedJarScanner.scan(rootVf = command.vf) } + var sourceId = command.vf.toNioPathOrNull()?.pathString ?: command.vf.path + if (sourceId.endsWith(JAR_SEPARATOR)) { + sourceId = sourceId.substringBeforeLast(JAR_SEPARATOR) + } + libraryIndex.indexSource(sourceId) { CombinedJarScanner.scan(rootVf = command.vf) } libraryIndexCount++ } 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 index 2a388b9447..6ee8113725 100644 --- 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 @@ -56,6 +56,7 @@ internal class KtSymbolIndex( ) private val scanningWorker = ScanningWorker( + sourceIndex = sourceIndex, indexWorker = indexWorker, modules = modules, ) 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 index 5cdc16cbc5..4e1c592c2a 100644 --- 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 @@ -3,10 +3,12 @@ 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, ) { @@ -35,6 +37,8 @@ internal class ScanningWorker( .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)) 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/services/ProjectStructureProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt index 7e63ee2494..f663cc0e57 100644 --- 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 @@ -3,19 +3,23 @@ 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() { @@ -27,7 +31,7 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr private lateinit var project: Project private val inMemoryVfToModule = ConcurrentHashMap() - private val pathToInMemoryVf = ConcurrentHashMap() + private val pathToInMemoryVf = ConcurrentHashMap() fun registerInMemoryFile(sourcePath: String, vf: VirtualFile) { pathToInMemoryVf.remove(sourcePath)?.let { inMemoryVfToModule.remove(it) } @@ -63,18 +67,38 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr element: PsiElement, useSiteModule: KaModule? ): KaModule { - val virtualFile = element.containingFile.virtualFile + 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 -> - val foundModule = searchVirtualFileInModule(virtualFile, useSiteModule ?: module, visited) - if (foundModule != null) return foundModule + searchVirtualFileInModule(virtualFile, module, visited)?.let { return it } } - // fallback: path-based lookup + // Path-based fallback for in-memory LightVirtualFiles created by onFileContentChanged. findModuleForSourceId(virtualFile.path)?.let { return it } return NotUnderContentRootModule( @@ -124,7 +148,11 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr return notUnderContentRootModuleWithoutPsiFile } - private fun searchVirtualFileInModule(vf: VirtualFile, module: KaModule, visited: MutableSet): KaModule? { + private fun searchVirtualFileInModule( + vf: VirtualFile, + module: KaModule, + visited: MutableSet + ): KaModule? { if (visited.contains(module)) return null if (module.contentScope.contains(vf)) return module 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 c76d3d5b0f..57ced1e5e5 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 @@ -103,6 +103,7 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi text = textWithPlaceholder ).apply { originalFile = ktFile + originalKtFile = ktFile } } @@ -131,20 +132,18 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi } context(ctx) { - runBlocking { - val items = mutableListOf() - val completionContext = determineCompletionContext(ctx.psiElement) - when (completionContext) { - CompletionContext.Scope -> - collectScopeCompletions(to = items) - - CompletionContext.Member -> - collectMemberCompletions(to = items) - } - - collectKeywordCompletions(to = items) - CompletionResult(items) + val items = mutableListOf() + val completionContext = determineCompletionContext(ctx.psiElement) + when (completionContext) { + CompletionContext.Scope -> + collectScopeCompletions(to = items) + + CompletionContext.Member -> + collectMemberCompletions(to = items) } + + collectKeywordCompletions(to = items) + CompletionResult(items) } } } @@ -244,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 @@ -281,7 +280,7 @@ 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 @@ -289,7 +288,7 @@ private suspend fun KaSession.collectUnimportedSymbols( // Library symbols: JAR-based, use full SymbolVisibilityChecker val visibilityChecker = env.symbolVisibilityChecker - env.libraryIndex?.findByPrefix(ctx.partial) + env.libraryIndex?.findByPrefix(ctx.partial, limit = 0) ?.forEach { symbol -> val isVisible = visibilityChecker.isVisible( symbol = symbol, @@ -301,7 +300,7 @@ private suspend fun KaSession.collectUnimportedSymbols( } // Source symbols: project .kt files — skip private and same-package symbols - env.sourceIndex?.findByPrefix(ctx.partial) + env.sourceIndex?.findByPrefix(ctx.partial, limit = 0) ?.forEach { symbol -> if (symbol.packageName == currentPackage) return@forEach 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 db61fb038f..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 @@ -5,11 +5,16 @@ 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 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>() 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 From 49542ad6b36a4fd01e79d97cacf92fcba5e8f89c Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Apr 2026 22:28:41 +0530 Subject: [PATCH 45/58] fix: remove library indexing logic for kt index worker Libraries are already indexed by JvmLibraryIndexingService Signed-off-by: Akash Yadav --- .../lsp/kotlin/compiler/index/IndexCommand.kt | 1 - .../lsp/kotlin/compiler/index/IndexWorker.kt | 18 +----------------- .../lsp/kotlin/compiler/index/KtSymbolIndex.kt | 1 - .../kotlin/compiler/index/ScanningWorker.kt | 11 +---------- 4 files changed, 2 insertions(+), 29 deletions(-) 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 index 3ac737e182..1d890426c1 100644 --- 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 @@ -10,5 +10,4 @@ internal sealed interface IndexCommand { data class ScanSourceFile(val vf: VirtualFile): IndexCommand data class IndexModifiedFile(val ktFile: KtFile): IndexCommand data class IndexSourceFile(val vf: VirtualFile): IndexCommand - data class IndexLibraryFile(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 index 3195eb17c6..3be16fdecc 100644 --- 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 @@ -1,24 +1,19 @@ package com.itsaky.androidide.lsp.kotlin.compiler.index import com.itsaky.androidide.lsp.kotlin.compiler.read -import com.itsaky.androidide.lsp.kotlin.utils.toNioPathOrNull -import org.appdevforall.codeonthego.indexing.jvm.CombinedJarScanner 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.com.intellij.util.io.URLUtil.JAR_SEPARATOR import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory -import kotlin.io.path.pathString internal class IndexWorker( private val project: Project, private val queue: WorkerQueue, private val fileIndex: KtFileMetadataIndex, private val sourceIndex: JvmSymbolIndex, - private val libraryIndex: JvmSymbolIndex, ) { companion object { private val logger = LoggerFactory.getLogger(IndexWorker::class.java) @@ -27,19 +22,9 @@ internal class IndexWorker( suspend fun start() { var scanCount = 0 var sourceIndexCount = 0 - var libraryIndexCount = 0 while (true) { when (val command = queue.take()) { - is IndexCommand.IndexLibraryFile -> { - var sourceId = command.vf.toNioPathOrNull()?.pathString ?: command.vf.path - if (sourceId.endsWith(JAR_SEPARATOR)) { - sourceId = sourceId.substringBeforeLast(JAR_SEPARATOR) - } - libraryIndex.indexSource(sourceId) { CombinedJarScanner.scan(rootVf = command.vf) } - libraryIndexCount++ - } - is IndexCommand.IndexSourceFile -> { if (command.vf.fileSystem.protocol != "file") { logger.warn("Unknown source file protocol: {}", command.vf.path) @@ -67,10 +52,9 @@ internal class IndexWorker( IndexCommand.IndexingComplete -> { logger.info( - "Indexing complete: scanned={}, sourceIndexCount={}, libraryIndexCount={}", + "Indexing complete: scanned={}, sourceIndexCount={}", scanCount, sourceIndexCount, - libraryIndexCount ) } 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 index 6ee8113725..822cc0c296 100644 --- 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 @@ -52,7 +52,6 @@ internal class KtSymbolIndex( queue = workerQueue, fileIndex = fileIndex, sourceIndex = sourceIndex, - libraryIndex = libraryIndex, ) private val scanningWorker = ScanningWorker( 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 index 4e1c592c2a..0f5a7c7db3 100644 --- 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 @@ -29,9 +29,7 @@ internal class ScanningWorker( } private suspend fun scan() { - val allModules = modules.asFlatSequence().toList() - val sourceFiles = allModules - .asSequence() + val sourceFiles = modules.asFlatSequence() .filter { it.isSourceModule } .flatMap { it.computeFiles(extended = true) } .takeWhile { isRunning.get() } @@ -52,13 +50,6 @@ internal class ScanningWorker( indexWorker.submitCommand(IndexCommand.IndexSourceFile(sourceFile)) } - allModules - .asSequence() - .filterNot { it.isSourceModule } - .flatMap { it.computeFiles(extended = false) } - .takeWhile { isRunning.get() } - .forEach { indexWorker.submitCommand(IndexCommand.IndexLibraryFile(it)) } - indexWorker.submitCommand(IndexCommand.IndexingComplete) } From 888193fcd69b19bd4a40d4411fe38146b708e3c9 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Apr 2026 22:47:26 +0530 Subject: [PATCH 46/58] feat: add index for generated JARs This indexed is *always* refreshed after every successful build Signed-off-by: Akash Yadav --- .../indexing/service/IndexingService.kt | 8 ++ .../service/IndexingServiceManager.kt | 14 ++ .../androidide/lsp/java/JavaLanguageServer.kt | 4 + .../jvm/JvmGeneratedIndexingService.kt | 132 ++++++++++++++++++ .../kotlin/compiler/CompilationEnvironment.kt | 3 + .../lsp/kotlin/compiler/KotlinProjectModel.kt | 8 ++ .../kotlin/completion/KotlinCompletions.kt | 7 + 7 files changed, 176 insertions(+) create mode 100644 lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmGeneratedIndexingService.kt 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 724804e7a5..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 @@ -69,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/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 995944e565..2bdb29845b 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 @@ -74,6 +74,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.appdevforall.codeonthego.indexing.jvm.JvmGeneratedIndexingService import org.appdevforall.codeonthego.indexing.jvm.JvmLibraryIndexingService import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -123,6 +124,9 @@ class JavaLanguageServer : ILanguageServer { projectManager.indexingServiceManager.register( service = JvmLibraryIndexingService(context = BaseApplication.baseInstance) ) + projectManager.indexingServiceManager.register( + service = JvmGeneratedIndexingService(context = BaseApplication.baseInstance) + ) JavaSnippetRepository.init() } 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/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 2389525252..78ff370699 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 @@ -148,6 +148,9 @@ internal class CompilationEnvironment( 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 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 04b6797d0c..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 @@ -4,6 +4,7 @@ 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.Workspace +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.JvmSymbolIndex import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex @@ -51,6 +52,13 @@ internal class KotlinProjectModel { .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. */ 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 57ced1e5e5..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 @@ -314,6 +314,13 @@ private fun KaSession.collectUnimportedSymbols( buildUnimportedSymbolItem(symbol)?.let { to += it } } + + // 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 } + } } context(ctx: AnalysisContext) From f3997a5ccb0f40ec3bef155a22d1ff90139f6b1f Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 15 Apr 2026 00:44:03 +0530 Subject: [PATCH 47/58] fix: java source files are not recognized by analysis API Signed-off-by: Akash Yadav --- .../kotlin/compiler/CompilationEnvironment.kt | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) 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 78ff370699..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 @@ -55,6 +55,7 @@ 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.command.CommandProcessor @@ -86,6 +87,8 @@ 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 /** @@ -237,7 +240,8 @@ internal class CompilationEnvironment( .asFlatSequence() .filterNot { it.isSourceModule } .flatMap { libMod -> - libMod.computeFiles(extended = false).map { file -> JavaRoot(file, JavaRoot.RootType.BINARY) } + libMod.computeFiles(extended = false) + .map { file -> JavaRoot(file, JavaRoot.RootType.BINARY) } } .toList() @@ -257,14 +261,21 @@ internal class CompilationEnvironment( addRoots(libraryRoots, MessageCollector.NONE) } + 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 = false).apply { + JvmDependenciesDynamicCompoundIndex(shouldOnlyFindFirstClass = true).apply { addIndex( JvmDependenciesIndexImpl( - libraryRoots, - shouldOnlyFindFirstClass = false + libraryRoots + javaRoots.map { JavaRoot(it, JavaRoot.RootType.SOURCE) }, + shouldOnlyFindFirstClass = true ) - ) // TODO Should receive all (sources + libraries) + ) indexedRoots.forEach { javaRoot -> if (javaRoot.file.isDirectory) { @@ -281,7 +292,12 @@ internal class CompilationEnvironment( javaFileManager.initialize( index = rootsIndex, packagePartProviders = listOf(packagePartProvider), - singleJavaFileRootsIndex = SingleJavaFileRootsIndex(emptyList()), + singleJavaFileRootsIndex = SingleJavaFileRootsIndex(singleJavaFileRoots.map { + JavaRoot( + it, + JavaRoot.RootType.SOURCE + ) + }), usePsiClassFilesReading = true, perfManager = null, ) @@ -367,7 +383,8 @@ internal class CompilationEnvironment( newKtFile.backingFilePath = path // Tell ProjectStructureProvider which module owns this LightVirtualFile. - val provider = project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider + val provider = + project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider provider.registerInMemoryFile(path.pathString, newKtFile.virtualFile) ktSymbolIndex.openKtFile(path, newKtFile) From a0e3075e8b319050b7276d48071e2aadb1014f00 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 15 Apr 2026 12:06:12 +0530 Subject: [PATCH 48/58] fix: remove unused kotlin lsp modules Signed-off-by: Akash Yadav --- lsp/jvm-symbol-index/build.gradle.kts | 1 - lsp/kotlin-core/build.gradle.kts | 47 - .../lsp/kotlin/index/ClasspathIndex.kt | 136 -- .../lsp/kotlin/index/ClasspathIndexer.kt | 883 --------- .../lsp/kotlin/index/DependencyTracker.kt | 90 - .../lsp/kotlin/index/ExtensionIndex.kt | 222 --- .../codeonthego/lsp/kotlin/index/FileIndex.kt | 260 --- .../lsp/kotlin/index/IndexEntry.kt | 357 ---- .../lsp/kotlin/index/PackageIndex.kt | 144 -- .../lsp/kotlin/index/ProjectIndex.kt | 454 ----- .../lsp/kotlin/index/StdlibIndex.kt | 259 --- .../lsp/kotlin/index/StdlibIndexLoader.kt | 461 ----- .../lsp/kotlin/index/SymbolIndex.kt | 428 ----- .../lsp/kotlin/parser/KotlinParser.kt | 221 --- .../lsp/kotlin/parser/ParseResult.kt | 315 --- .../codeonthego/lsp/kotlin/parser/Position.kt | 105 - .../lsp/kotlin/parser/PositionConverter.kt | 109 -- .../lsp/kotlin/parser/SyntaxKind.kt | 1048 ---------- .../lsp/kotlin/parser/SyntaxNode.kt | 519 ----- .../lsp/kotlin/parser/SyntaxTree.kt | 220 --- .../lsp/kotlin/parser/SyntaxVisitor.kt | 492 ----- .../lsp/kotlin/parser/TextRange.kt | 253 --- .../lsp/kotlin/semantic/AnalysisContext.kt | 264 --- .../lsp/kotlin/semantic/Diagnostic.kt | 489 ----- .../lsp/kotlin/semantic/OverloadResolver.kt | 495 ----- .../lsp/kotlin/semantic/SemanticAnalyzer.kt | 915 --------- .../lsp/kotlin/semantic/SymbolResolver.kt | 1116 ----------- .../lsp/kotlin/semantic/TypeInferrer.kt | 1146 ----------- .../lsp/kotlin/server/AnalysisCache.kt | 123 -- .../lsp/kotlin/server/AnalysisScheduler.kt | 326 ---- .../lsp/kotlin/server/DocumentManager.kt | 264 --- .../lsp/kotlin/server/DocumentState.kt | 302 --- .../lsp/kotlin/server/KotlinLanguageServer.kt | 282 --- .../server/KotlinTextDocumentService.kt | 263 --- .../kotlin/server/KotlinWorkspaceService.kt | 162 -- .../server/providers/CodeActionProvider.kt | 214 --- .../server/providers/CompletionProvider.kt | 1267 ------------ .../server/providers/DefinitionProvider.kt | 319 --- .../server/providers/DiagnosticProvider.kt | 120 -- .../providers/DocumentSymbolProvider.kt | 154 -- .../kotlin/server/providers/HoverProvider.kt | 336 ---- .../server/providers/SemanticTokenProvider.kt | 467 ----- .../lsp/kotlin/symbol/Modifiers.kt | 247 --- .../codeonthego/lsp/kotlin/symbol/Scope.kt | 298 --- .../lsp/kotlin/symbol/ScopeKind.kt | 199 -- .../codeonthego/lsp/kotlin/symbol/Symbol.kt | 657 ------- .../lsp/kotlin/symbol/SymbolBuilder.kt | 1706 ----------------- .../lsp/kotlin/symbol/SymbolLocation.kt | 111 -- .../lsp/kotlin/symbol/SymbolTable.kt | 329 ---- .../lsp/kotlin/symbol/SymbolVisitor.kt | 136 -- .../lsp/kotlin/symbol/Visibility.kt | 91 - .../lsp/kotlin/types/KotlinType.kt | 564 ------ .../lsp/kotlin/types/TypeChecker.kt | 433 ----- .../lsp/kotlin/types/TypeResolver.kt | 389 ---- lsp/kotlin-stdlib-generator/build.gradle.kts | 43 - .../kotlin/generator/StdlibIndexGenerator.kt | 1284 ------------- settings.gradle.kts | 2 - 57 files changed, 22537 deletions(-) delete mode 100644 lsp/kotlin-core/build.gradle.kts delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndex.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndexer.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/DependencyTracker.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ExtensionIndex.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/FileIndex.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/IndexEntry.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/PackageIndex.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ProjectIndex.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndex.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndexLoader.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/SymbolIndex.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/KotlinParser.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/ParseResult.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/Position.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/PositionConverter.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxKind.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxNode.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxTree.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxVisitor.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/TextRange.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/AnalysisContext.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/Diagnostic.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/OverloadResolver.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SemanticAnalyzer.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SymbolResolver.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/TypeInferrer.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisCache.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisScheduler.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentManager.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentState.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinLanguageServer.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinTextDocumentService.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinWorkspaceService.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CodeActionProvider.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CompletionProvider.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DefinitionProvider.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DiagnosticProvider.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DocumentSymbolProvider.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/HoverProvider.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/SemanticTokenProvider.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Modifiers.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Scope.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/ScopeKind.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Symbol.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolBuilder.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolLocation.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolTable.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolVisitor.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Visibility.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/KotlinType.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/TypeChecker.kt delete mode 100644 lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/TypeResolver.kt delete mode 100644 lsp/kotlin-stdlib-generator/build.gradle.kts delete mode 100644 lsp/kotlin-stdlib-generator/src/main/kotlin/org/appdevforall/codeonthego/lsp/kotlin/generator/StdlibIndexGenerator.kt diff --git a/lsp/jvm-symbol-index/build.gradle.kts b/lsp/jvm-symbol-index/build.gradle.kts index 796860d0e3..959f2264be 100644 --- a/lsp/jvm-symbol-index/build.gradle.kts +++ b/lsp/jvm-symbol-index/build.gradle.kts @@ -21,5 +21,4 @@ dependencies { api(projects.lsp.jvmSymbolModels) api(projects.subprojects.kotlinAnalysisApi) api(projects.subprojects.projects) - api(projects.lsp.kotlinCore) } diff --git a/lsp/kotlin-core/build.gradle.kts b/lsp/kotlin-core/build.gradle.kts deleted file mode 100644 index 29489689fc..0000000000 --- a/lsp/kotlin-core/build.gradle.kts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -plugins { - id("com.android.library") - id("kotlin-android") - kotlin("plugin.serialization") -} - -android { - namespace = "org.appdevforall.codeonthego.lsp.kotlin" -} - -kotlin { - compilerOptions { - freeCompilerArgs.add("-opt-in=kotlin.contracts.ExperimentalContracts") - } -} - -dependencies { - implementation(libs.androidide.ts) - implementation(libs.androidide.ts.kotlin) - - implementation(libs.common.lsp4j) - implementation(libs.common.jsonrpc) - - implementation(libs.common.kotlin.coroutines.core) - implementation(libs.common.kotlin.coroutines.android) - - implementation(libs.kotlinx.serialization.json) - - testImplementation(libs.tests.junit.jupiter) -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndex.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndex.kt deleted file mode 100644 index 47085ff0dd..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndex.kt +++ /dev/null @@ -1,136 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - - -class ClasspathIndex : SymbolIndex { - - private val symbolsByFqName = mutableMapOf() - private val symbolsByName = mutableMapOf>() - private val symbolsByPackage = mutableMapOf>() - private val extensionsByReceiver = mutableMapOf>() - private val allSymbols = mutableListOf() - private val sourceJars = mutableSetOf() - - override val size: Int get() = allSymbols.size - - val packageNames: Set get() = symbolsByPackage.keys.toSet() - - val jarCount: Int get() = sourceJars.size - - override fun findByFqName(fqName: String): IndexedSymbol? { - return symbolsByFqName[fqName] - } - - override fun findBySimpleName(name: String): List { - return symbolsByName[name]?.toList() ?: emptyList() - } - - override fun findByPackage(packageName: String): List { - return symbolsByPackage[packageName]?.toList() ?: emptyList() - } - - override fun findByPrefix(prefix: String, limit: Int): List { - val results = symbolsByName.keys - .filter { it.startsWith(prefix, ignoreCase = true) } - .flatMap { symbolsByName[it] ?: emptyList() } - - return if (limit > 0) results.take(limit) else results - } - - override fun getAllClasses(): List { - return allSymbols.filter { it.kind.isClass } - } - - override fun getAllFunctions(): List { - return allSymbols.filter { it.kind == IndexedSymbolKind.FUNCTION } - } - - override fun getAllProperties(): List { - return allSymbols.filter { it.kind == IndexedSymbolKind.PROPERTY } - } - - fun getAllSymbols(): List { - return allSymbols.toList() - } - - fun findMembers(classFqName: String): List { - return allSymbols.filter { it.containingClass == classFqName } - } - - fun findExtensionsFor(receiverType: String): List { - val results = mutableListOf() - extensionsByReceiver[receiverType]?.let { results.addAll(it) } - val simpleName = receiverType.substringAfterLast('.') - if (simpleName != receiverType) { - extensionsByReceiver[simpleName]?.let { results.addAll(it) } - } - return results - } - - fun hasPackage(packageName: String): Boolean { - return symbolsByPackage.containsKey(packageName) - } - - fun getSubpackages(parentPackage: String): List { - val prefix = if (parentPackage.isEmpty()) "" else "$parentPackage." - return symbolsByPackage.keys - .filter { it.startsWith(prefix) && it != parentPackage } - .map { name -> - val rest = name.removePrefix(prefix) - val firstDot = rest.indexOf('.') - if (firstDot > 0) rest.substring(0, firstDot) else rest - } - .distinct() - .map { if (parentPackage.isEmpty()) it else "$parentPackage.$it" } - } - - internal fun addSymbol(symbol: IndexedSymbol) { - if (symbolsByFqName.containsKey(symbol.fqName)) { - return - } - - allSymbols.add(symbol) - symbolsByFqName[symbol.fqName] = symbol - symbolsByName.getOrPut(symbol.name) { mutableListOf() }.add(symbol) - - if (symbol.isTopLevel && !symbol.isExtension) { - symbolsByPackage.getOrPut(symbol.packageName) { mutableListOf() }.add(symbol) - } - - if (symbol.isExtension && symbol.receiverType != null) { - extensionsByReceiver.getOrPut(symbol.receiverType) { mutableListOf() }.add(symbol) - val simpleName = symbol.receiverType.substringAfterLast('.') - if (simpleName != symbol.receiverType) { - extensionsByReceiver.getOrPut(simpleName) { mutableListOf() }.add(symbol) - } - } - } - - internal fun addAll(symbols: Iterable) { - symbols.forEach { addSymbol(it) } - } - - internal fun addSourceJar(jarPath: String) { - sourceJars.add(jarPath) - } - - internal fun hasJar(jarPath: String): Boolean { - return sourceJars.contains(jarPath) - } - - fun clear() { - allSymbols.clear() - symbolsByFqName.clear() - symbolsByName.clear() - symbolsByPackage.clear() - extensionsByReceiver.clear() - sourceJars.clear() - } - - override fun toString(): String { - return "ClasspathIndex(jars=$jarCount, symbols=$size)" - } - - companion object { - fun empty(): ClasspathIndex = ClasspathIndex() - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndexer.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndexer.kt deleted file mode 100644 index 3b3eaf73b7..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ClasspathIndexer.kt +++ /dev/null @@ -1,883 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Visibility -import java.io.File -import java.io.InputStream -import java.util.jar.JarFile -import java.util.zip.ZipFile - -class ClasspathIndexer { - - fun index(files: List): ClasspathIndex { - val index = ClasspathIndex() - - for (file in files) { - if (!file.exists()) continue - - when { - file.name.endsWith(".jar") -> indexJar(file, index) - file.name.endsWith(".aar") -> indexAar(file, index) - file.isDirectory -> indexDirectory(file, index) - } - } - - return index - } - - fun indexIncremental(files: List, existingIndex: ClasspathIndex): ClasspathIndex { - for (file in files) { - if (!file.exists()) continue - if (existingIndex.hasJar(file.absolutePath)) continue - - when { - file.name.endsWith(".jar") -> indexJar(file, existingIndex) - file.name.endsWith(".aar") -> indexAar(file, existingIndex) - file.isDirectory -> indexDirectory(file, existingIndex) - } - } - - return existingIndex - } - - private fun indexJar(jarFile: File, index: ClasspathIndex) { - try { - JarFile(jarFile).use { jar -> - val entries = jar.entries() - while (entries.hasMoreElements()) { - val entry = entries.nextElement() - if (entry.name.endsWith(".class") && !entry.name.contains('$')) { - val className = entry.name - .removeSuffix(".class") - .replace('/', '.') - - if (shouldIndex(className)) { - jar.getInputStream(entry).use { inputStream -> - val symbols = parseClassFile(className, inputStream, jarFile.absolutePath) - index.addAll(symbols) - } - } - } - } - index.addSourceJar(jarFile.absolutePath) - } - } catch (e: Exception) { - // Silently skip corrupted jars - } - } - - private fun indexAar(aarFile: File, index: ClasspathIndex) { - try { - ZipFile(aarFile).use { aar -> - val classesJar = aar.getEntry("classes.jar") ?: return - - val tempFile = File.createTempFile("classes", ".jar") - tempFile.deleteOnExit() - - aar.getInputStream(classesJar).use { input -> - tempFile.outputStream().use { output -> - input.copyTo(output) - } - } - - indexJar(tempFile, index) - index.addSourceJar(aarFile.absolutePath) - - tempFile.delete() - } - } catch (e: Exception) { - // Silently skip corrupted aars - } - } - - private fun indexDirectory(directory: File, index: ClasspathIndex) { - directory.walkTopDown() - .filter { it.isFile && it.extension == "class" && !it.name.contains('$') } - .forEach { classFile -> - val relativePath = classFile.relativeTo(directory).path - val className = relativePath - .removeSuffix(".class") - .replace(File.separatorChar, '.') - - if (shouldIndex(className)) { - classFile.inputStream().use { inputStream -> - val symbols = parseClassFile(className, inputStream, directory.absolutePath) - index.addAll(symbols) - } - } - } - index.addSourceJar(directory.absolutePath) - } - - private fun detectExtensionReceiver(method: MethodInfo): String? { - if (!method.isExtension || (method.parameters.isEmpty())) return null - return method.parameters.first().type - } - - private fun shouldIndex(className: String): Boolean { - if (className.startsWith("java.") || className.startsWith("javax.")) { - return false - } - if (className.contains(".internal.") || className.contains(".impl.")) { - return false - } - if (className.startsWith("sun.") || className.startsWith("com.sun.")) { - return false - } - return true - } - - private fun parseClassFile(className: String, inputStream: InputStream, sourcePath: String): List { - val symbols = mutableListOf() - - try { - val classInfo = ClassFileReader.read(inputStream) - - val packageName = className.substringBeforeLast('.', "") - val simpleName = className.substringAfterLast('.') - - val classSymbol = IndexedSymbol( - name = simpleName, - fqName = className, - kind = classInfo.kind, - packageName = packageName, - visibility = classInfo.visibility, - typeParameters = classInfo.typeParameters, - superTypes = classInfo.superTypes, - filePath = sourcePath - ) - symbols.add(classSymbol) - - for (method in classInfo.methods) { - if (method.visibility != Visibility.PUBLIC && method.visibility != Visibility.PROTECTED) { - continue - } - if (method.name.startsWith("access$") || method.name.contains('$')) { - continue - } - if (method.name == "") continue - - val extensionReceiver = detectExtensionReceiver(method) - - val params = if (extensionReceiver != null && method.parameters.isNotEmpty()) { - method.parameters.drop(1) - } else { - method.parameters - } - - val methodSymbol = IndexedSymbol( - name = method.name, - fqName = "$className.${method.name}", - kind = if (method.name == "") IndexedSymbolKind.CONSTRUCTOR else IndexedSymbolKind.FUNCTION, - packageName = packageName, - containingClass = if (extensionReceiver != null) null else className, - visibility = method.visibility, - parameters = params, - returnType = method.returnType, - receiverType = extensionReceiver, - typeParameters = method.typeParameters, - filePath = sourcePath - ) - symbols.add(methodSymbol) - } - - for (field in classInfo.fields) { - if (field.visibility != Visibility.PUBLIC && field.visibility != Visibility.PROTECTED) { - continue - } - if (field.name.contains('$')) { - continue - } - - val fieldSymbol = IndexedSymbol( - name = field.name, - fqName = "$className.${field.name}", - kind = IndexedSymbolKind.PROPERTY, - packageName = packageName, - containingClass = className, - visibility = field.visibility, - returnType = field.type, - filePath = sourcePath - ) - symbols.add(fieldSymbol) - } - - } catch (e: Exception) { - val packageName = className.substringBeforeLast('.', "") - val simpleName = className.substringAfterLast('.') - symbols.add(IndexedSymbol( - name = simpleName, - fqName = className, - kind = IndexedSymbolKind.CLASS, - packageName = packageName, - visibility = Visibility.PUBLIC, - filePath = sourcePath - )) - } - - return symbols - } -} - -internal object ClassFileReader { - - fun read(inputStream: InputStream): ClassInfo { - val bytes = inputStream.readBytes() - return parseClass(bytes) - } - - private fun parseClass(bytes: ByteArray): ClassInfo { - if (bytes.size < 10) { - return ClassInfo.EMPTY - } - - val magic = readU4(bytes, 0) - if (magic != 0xCAFEBABE.toInt()) { - return ClassInfo.EMPTY - } - - val constantPoolCount = readU2(bytes, 8) - val constantPool = parseConstantPool(bytes, constantPoolCount) - - var offset = 10 - for (i in 1 until constantPoolCount) { - val tag = bytes[offset].toInt() and 0xFF - offset += constantPoolEntrySize(tag, bytes, offset) - } - - val accessFlags = readU2(bytes, offset) - offset += 2 - - val thisClassIndex = readU2(bytes, offset) - offset += 2 - - val superClassIndex = readU2(bytes, offset) - offset += 2 - - val interfacesCount = readU2(bytes, offset) - offset += 2 - val interfaces = mutableListOf() - for (i in 0 until interfacesCount) { - val interfaceIndex = readU2(bytes, offset) - offset += 2 - constantPool.getClassName(interfaceIndex)?.let { interfaces.add(it) } - } - - val fieldsCount = readU2(bytes, offset) - offset += 2 - val fields = mutableListOf() - for (i in 0 until fieldsCount) { - val fieldInfo = parseField(bytes, offset, constantPool) - fields.add(fieldInfo.first) - offset = fieldInfo.second - } - - val methodsCount = readU2(bytes, offset) - offset += 2 - val methods = mutableListOf() - for (i in 0 until methodsCount) { - val methodInfo = parseMethod(bytes, offset, constantPool) - methods.add(methodInfo.first) - offset = methodInfo.second - } - - val superTypes = mutableListOf() - constantPool.getClassName(superClassIndex)?.let { - if (it != "java.lang.Object") { - superTypes.add(it) - } - } - superTypes.addAll(interfaces) - - val attributesCount = readU2(bytes, offset) - offset += 2 - var extensionFunctions = emptyMap>() - for (i in 0 until attributesCount) { - val attrNameIndex = readU2(bytes, offset) - offset += 2 - val attrLength = readU4(bytes, offset) - offset += 4 - val attrName = constantPool.getUtf8(attrNameIndex) - if (attrName == "RuntimeVisibleAnnotations") { - extensionFunctions = parseKotlinMetadataExtensions(bytes, offset, attrLength, constantPool) - } - offset += attrLength - } - - val markedMethods = if (extensionFunctions.isNotEmpty()) { - val remaining = extensionFunctions.mapValues { it.value.toMutableList() }.toMutableMap() - methods.map { method -> - val valueCounts = remaining[method.name] ?: return@map method - if (method.parameters.isEmpty()) return@map method - val idx = valueCounts.indexOf(method.parameters.size - 1) - if (idx < 0) return@map method - valueCounts.removeAt(idx) - method.copy(isExtension = true) - } - } else { - methods - } - - return ClassInfo( - kind = parseClassKind(accessFlags), - visibility = parseVisibility(accessFlags), - typeParameters = emptyList(), - superTypes = superTypes, - methods = markedMethods, - fields = fields - ) - } - - private fun parseKotlinMetadataExtensions( - bytes: ByteArray, - offset: Int, - length: Int, - constantPool: ConstantPool - ): Map> { - try { - val endOffset = offset + length - if (offset >= bytes.size) return emptyMap() - val numAnnotations = readU2(bytes, offset) - var pos = offset + 2 - - for (a in 0 until numAnnotations) { - if (pos >= endOffset) break - val typeIndex = readU2(bytes, pos) - pos += 2 - val typeName = constantPool.getUtf8(typeIndex) ?: "" - val numPairs = readU2(bytes, pos) - pos += 2 - - if (typeName != "Lkotlin/Metadata;") { - for (p in 0 until numPairs) { - pos = skipAnnotationElementValue(bytes, pos + 2) - } - continue - } - - var d1Bytes: List = emptyList() - var d2Strings: List = emptyList() - - for (p in 0 until numPairs) { - val nameIdx = readU2(bytes, pos) - pos += 2 - val pairName = constantPool.getUtf8(nameIdx) ?: "" - - when (pairName) { - "d1" -> { - val result = parseAnnotationArrayOfStrings(bytes, pos, constantPool) - d1Bytes = result.first.map { it.toByteArray(Charsets.ISO_8859_1) } - pos = result.second - } - "d2" -> { - val result = parseAnnotationArrayOfStrings(bytes, pos, constantPool) - d2Strings = result.first - pos = result.second - } - else -> { - pos = skipAnnotationElementValue(bytes, pos) - } - } - } - - if (d1Bytes.isNotEmpty() && d2Strings.isNotEmpty()) { - val combined = d1Bytes.fold(ByteArray(0)) { acc, b -> acc + b } - return extractExtensionFunctions(combined, d2Strings) - } - } - } catch (_: Exception) { - } - return emptyMap() - } - - private fun parseAnnotationArrayOfStrings( - bytes: ByteArray, - offset: Int, - constantPool: ConstantPool - ): Pair, Int> { - if (bytes[offset].toInt().toChar() != '[') { - return emptyList() to skipAnnotationElementValue(bytes, offset) - } - var pos = offset + 1 - val count = readU2(bytes, pos) - pos += 2 - val strings = mutableListOf() - for (i in 0 until count) { - val tag = bytes[pos].toInt().toChar() - pos++ - if (tag == 's') { - val idx = readU2(bytes, pos) - pos += 2 - constantPool.getUtf8(idx)?.let { strings.add(it) } - } else { - pos = skipAnnotationElementValueAfterTag(bytes, tag, pos) - } - } - return strings to pos - } - - private fun skipAnnotationElementValue(bytes: ByteArray, offset: Int): Int { - val tag = bytes[offset].toInt().toChar() - return skipAnnotationElementValueAfterTag(bytes, tag, offset + 1) - } - - private fun skipAnnotationElementValueAfterTag(bytes: ByteArray, tag: Char, offset: Int): Int { - return when (tag) { - 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z', 's' -> offset + 2 - 'e' -> offset + 4 - 'c' -> offset + 2 - '@' -> { - var pos = offset + 2 - val numPairs = readU2(bytes, pos) - pos += 2 - for (i in 0 until numPairs) { - pos = skipAnnotationElementValue(bytes, pos + 2) - } - pos - } - '[' -> { - val count = readU2(bytes, offset) - var pos = offset + 2 - for (i in 0 until count) { - pos = skipAnnotationElementValue(bytes, pos) - } - pos - } - else -> offset + 2 - } - } - - private fun extractExtensionFunctions(d1: ByteArray, d2: List): Map> { - val extensionFunctions = mutableMapOf>() - try { - var pos = 0 - while (pos < d1.size) { - val byte = d1[pos].toInt() and 0xFF - val fieldNumber = byte ushr 3 - val wireType = byte and 0x07 - pos++ - - if (fieldNumber == 0 && wireType == 0) break - - when (wireType) { - 0 -> { - while (pos < d1.size && (d1[pos].toInt() and 0x80) != 0) pos++ - pos++ - } - 1 -> pos += 8 - 2 -> { - val (msgLen, newPos) = readVarInt(d1, pos) - pos = newPos - - if (fieldNumber == 9) { - val msgEnd = pos + msgLen - var nameIndex = -1 - var hasReceiver = false - var valueParamCount = 0 - var innerPos = pos - - while (innerPos < msgEnd) { - val innerByte = d1[innerPos].toInt() and 0xFF - val innerField = innerByte ushr 3 - val innerWire = innerByte and 0x07 - innerPos++ - - when (innerWire) { - 0 -> { - var value = 0 - var shift = 0 - while (innerPos < msgEnd) { - val b = d1[innerPos].toInt() and 0xFF - innerPos++ - value = value or ((b and 0x7F) shl shift) - if ((b and 0x80) == 0) break - shift += 7 - } - if (innerField == 2) nameIndex = value - } - 1 -> innerPos += 8 - 2 -> { - val (innerLen, np) = readVarInt(d1, innerPos) - innerPos = np - if (innerField == 5) valueParamCount++ - if (innerField == 6) hasReceiver = true - innerPos += innerLen - } - 5 -> innerPos += 4 - else -> break - } - } - - if (hasReceiver && nameIndex >= 0 && nameIndex < d2.size) { - extensionFunctions - .getOrPut(d2[nameIndex]) { mutableListOf() } - .add(valueParamCount) - } - pos = msgEnd - } else { - pos += msgLen - } - } - 5 -> pos += 4 - else -> break - } - } - } catch (_: Exception) { - } - return extensionFunctions - } - - private fun readVarInt(bytes: ByteArray, startPos: Int): Pair { - var pos = startPos - var value = 0 - var shift = 0 - while (pos < bytes.size) { - val b = bytes[pos].toInt() and 0xFF - pos++ - value = value or ((b and 0x7F) shl shift) - if ((b and 0x80) == 0) break - shift += 7 - } - return value to pos - } - - private fun parseConstantPool(bytes: ByteArray, count: Int): ConstantPool { - val pool = ConstantPool(count) - var offset = 10 - - var i = 1 - while (i < count) { - val tag = bytes[offset].toInt() and 0xFF - when (tag) { - CONSTANT_Utf8 -> { - val length = readU2(bytes, offset + 1) - val value = String(bytes, offset + 3, length, Charsets.UTF_8) - pool.setUtf8(i, value) - offset += 3 + length - } - CONSTANT_Integer, CONSTANT_Float -> { - offset += 5 - } - CONSTANT_Long, CONSTANT_Double -> { - offset += 9 - i++ - } - CONSTANT_Class -> { - val nameIndex = readU2(bytes, offset + 1) - pool.setClass(i, nameIndex) - offset += 3 - } - CONSTANT_String -> { - offset += 3 - } - CONSTANT_Fieldref, CONSTANT_Methodref, CONSTANT_InterfaceMethodref -> { - offset += 5 - } - CONSTANT_NameAndType -> { - val nameIndex = readU2(bytes, offset + 1) - val descriptorIndex = readU2(bytes, offset + 3) - pool.setNameAndType(i, nameIndex, descriptorIndex) - offset += 5 - } - CONSTANT_MethodHandle -> { - offset += 4 - } - CONSTANT_MethodType -> { - offset += 3 - } - CONSTANT_Dynamic, CONSTANT_InvokeDynamic -> { - offset += 5 - } - CONSTANT_Module, CONSTANT_Package -> { - offset += 3 - } - else -> { - offset += 3 - } - } - i++ - } - - return pool - } - - private fun constantPoolEntrySize(tag: Int, bytes: ByteArray, offset: Int): Int { - return when (tag) { - CONSTANT_Utf8 -> 3 + readU2(bytes, offset + 1) - CONSTANT_Integer, CONSTANT_Float -> 5 - CONSTANT_Long, CONSTANT_Double -> 9 - CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType, CONSTANT_Module, CONSTANT_Package -> 3 - CONSTANT_Fieldref, CONSTANT_Methodref, CONSTANT_InterfaceMethodref, - CONSTANT_NameAndType, CONSTANT_Dynamic, CONSTANT_InvokeDynamic -> 5 - CONSTANT_MethodHandle -> 4 - else -> 3 - } - } - - private fun parseField(bytes: ByteArray, startOffset: Int, pool: ConstantPool): Pair { - var offset = startOffset - - val accessFlags = readU2(bytes, offset) - offset += 2 - - val nameIndex = readU2(bytes, offset) - offset += 2 - - val descriptorIndex = readU2(bytes, offset) - offset += 2 - - val attributesCount = readU2(bytes, offset) - offset += 2 - - for (i in 0 until attributesCount) { - offset += 2 - val attrLength = readU4(bytes, offset) - offset += 4 + attrLength - } - - val name = pool.getUtf8(nameIndex) ?: "unknown" - val descriptor = pool.getUtf8(descriptorIndex) ?: "" - val type = parseTypeDescriptor(descriptor) - - return FieldInfo( - name = name, - type = type, - visibility = parseVisibility(accessFlags) - ) to offset - } - - private fun parseMethod(bytes: ByteArray, startOffset: Int, pool: ConstantPool): Pair { - var offset = startOffset - - val accessFlags = readU2(bytes, offset) - offset += 2 - - val nameIndex = readU2(bytes, offset) - offset += 2 - - val descriptorIndex = readU2(bytes, offset) - offset += 2 - - val attributesCount = readU2(bytes, offset) - offset += 2 - - for (i in 0 until attributesCount) { - offset += 2 - val attrLength = readU4(bytes, offset) - offset += 4 + attrLength - } - - val name = pool.getUtf8(nameIndex) ?: "unknown" - val descriptor = pool.getUtf8(descriptorIndex) ?: "()" - - val (params, returnType) = parseMethodDescriptor(descriptor) - - return MethodInfo( - name = name, - parameters = params, - returnType = returnType, - visibility = parseVisibility(accessFlags), - typeParameters = emptyList() - ) to offset - } - - private fun parseClassKind(accessFlags: Int): IndexedSymbolKind { - return when { - (accessFlags and ACC_ANNOTATION) != 0 -> IndexedSymbolKind.ANNOTATION_CLASS - (accessFlags and ACC_ENUM) != 0 -> IndexedSymbolKind.ENUM_CLASS - (accessFlags and ACC_INTERFACE) != 0 -> IndexedSymbolKind.INTERFACE - else -> IndexedSymbolKind.CLASS - } - } - - private fun parseVisibility(accessFlags: Int): Visibility { - return when { - (accessFlags and ACC_PUBLIC) != 0 -> Visibility.PUBLIC - (accessFlags and ACC_PROTECTED) != 0 -> Visibility.PROTECTED - (accessFlags and ACC_PRIVATE) != 0 -> Visibility.PRIVATE - else -> Visibility.INTERNAL - } - } - - private fun parseTypeDescriptor(descriptor: String): String { - if (descriptor.isEmpty()) return "Any" - - return when (descriptor[0]) { - 'B' -> "Byte" - 'C' -> "Char" - 'D' -> "Double" - 'F' -> "Float" - 'I' -> "Int" - 'J' -> "Long" - 'S' -> "Short" - 'Z' -> "Boolean" - 'V' -> "Unit" - 'L' -> { - val endIndex = descriptor.indexOf(';') - if (endIndex > 1) { - descriptor.substring(1, endIndex).replace('/', '.').replace('$', '.') - } else "Any" - } - '[' -> { - val componentType = parseTypeDescriptor(descriptor.substring(1)) - "Array<$componentType>" - } - else -> "Any" - } - } - - private fun parseMethodDescriptor(descriptor: String): Pair, String> { - val params = mutableListOf() - - val paramsEnd = descriptor.indexOf(')') - if (paramsEnd < 0) return emptyList() to "Unit" - - var i = 1 - var paramIndex = 0 - while (i < paramsEnd) { - val (type, nextIndex) = parseTypeAtIndex(descriptor, i) - params.add(IndexedParameter( - name = "p$paramIndex", - type = type - )) - paramIndex++ - i = nextIndex - } - - val returnDescriptor = descriptor.substring(paramsEnd + 1) - val returnType = parseTypeDescriptor(returnDescriptor) - - return params to returnType - } - - private fun parseTypeAtIndex(descriptor: String, startIndex: Int): Pair { - if (startIndex >= descriptor.length) return "Any" to startIndex - - return when (descriptor[startIndex]) { - 'B' -> "Byte" to startIndex + 1 - 'C' -> "Char" to startIndex + 1 - 'D' -> "Double" to startIndex + 1 - 'F' -> "Float" to startIndex + 1 - 'I' -> "Int" to startIndex + 1 - 'J' -> "Long" to startIndex + 1 - 'S' -> "Short" to startIndex + 1 - 'Z' -> "Boolean" to startIndex + 1 - 'V' -> "Unit" to startIndex + 1 - 'L' -> { - val endIndex = descriptor.indexOf(';', startIndex) - if (endIndex > startIndex) { - val className = descriptor.substring(startIndex + 1, endIndex) - .replace('/', '.') - .replace('$', '.') - className to endIndex + 1 - } else { - "Any" to descriptor.length - } - } - '[' -> { - val (componentType, nextIndex) = parseTypeAtIndex(descriptor, startIndex + 1) - "Array<$componentType>" to nextIndex - } - else -> "Any" to startIndex + 1 - } - } - - private fun readU2(bytes: ByteArray, offset: Int): Int { - return ((bytes[offset].toInt() and 0xFF) shl 8) or - (bytes[offset + 1].toInt() and 0xFF) - } - - private fun readU4(bytes: ByteArray, offset: Int): Int { - return ((bytes[offset].toInt() and 0xFF) shl 24) or - ((bytes[offset + 1].toInt() and 0xFF) shl 16) or - ((bytes[offset + 2].toInt() and 0xFF) shl 8) or - (bytes[offset + 3].toInt() and 0xFF) - } - - private const val CONSTANT_Utf8 = 1 - private const val CONSTANT_Integer = 3 - private const val CONSTANT_Float = 4 - private const val CONSTANT_Long = 5 - private const val CONSTANT_Double = 6 - private const val CONSTANT_Class = 7 - private const val CONSTANT_String = 8 - private const val CONSTANT_Fieldref = 9 - private const val CONSTANT_Methodref = 10 - private const val CONSTANT_InterfaceMethodref = 11 - private const val CONSTANT_NameAndType = 12 - private const val CONSTANT_MethodHandle = 15 - private const val CONSTANT_MethodType = 16 - private const val CONSTANT_Dynamic = 17 - private const val CONSTANT_InvokeDynamic = 18 - private const val CONSTANT_Module = 19 - private const val CONSTANT_Package = 20 - - private const val ACC_PUBLIC = 0x0001 - private const val ACC_PRIVATE = 0x0002 - private const val ACC_PROTECTED = 0x0004 - private const val ACC_INTERFACE = 0x0200 - private const val ACC_ENUM = 0x4000 - private const val ACC_ANNOTATION = 0x2000 -} - -private class ConstantPool(size: Int) { - private val utf8Entries = arrayOfNulls(size) - private val classEntries = IntArray(size) - private val nameAndTypeEntries = Array(size) { intArrayOf(0, 0) } - - fun setUtf8(index: Int, value: String) { - utf8Entries[index] = value - } - - fun getUtf8(index: Int): String? { - return if (index > 0 && index < utf8Entries.size) utf8Entries[index] else null - } - - fun setClass(index: Int, nameIndex: Int) { - classEntries[index] = nameIndex - } - - fun getClassName(index: Int): String? { - if (index <= 0 || index >= classEntries.size) return null - val nameIndex = classEntries[index] - return getUtf8(nameIndex)?.replace('/', '.') - } - - fun setNameAndType(index: Int, nameIndex: Int, descriptorIndex: Int) { - nameAndTypeEntries[index] = intArrayOf(nameIndex, descriptorIndex) - } -} - -data class ClassInfo( - val kind: IndexedSymbolKind, - val visibility: Visibility, - val typeParameters: List, - val superTypes: List, - val methods: List, - val fields: List -) { - companion object { - val EMPTY = ClassInfo( - kind = IndexedSymbolKind.CLASS, - visibility = Visibility.PUBLIC, - typeParameters = emptyList(), - superTypes = emptyList(), - methods = emptyList(), - fields = emptyList() - ) - } -} - -data class MethodInfo( - val name: String, - val parameters: List, - val returnType: String, - val visibility: Visibility, - val typeParameters: List, - val isExtension: Boolean = false -) - -data class FieldInfo( - val name: String, - val type: String, - val visibility: Visibility -) diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/DependencyTracker.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/DependencyTracker.kt deleted file mode 100644 index 795c6ac2c6..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/DependencyTracker.kt +++ /dev/null @@ -1,90 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -import java.util.concurrent.ConcurrentHashMap - -class DependencyTracker { - private val dependsOn = ConcurrentHashMap>() - private val dependedBy = ConcurrentHashMap>() - - fun addDependency(fromUri: String, toSymbolFqName: String) { - dependsOn.getOrPut(fromUri) { ConcurrentHashMap.newKeySet() }.add(toSymbolFqName) - dependedBy.getOrPut(toSymbolFqName) { ConcurrentHashMap.newKeySet() }.add(fromUri) - } - - fun addImportDependency(fromUri: String, importedFqName: String) { - addDependency(fromUri, importedFqName) - } - - fun addTypeDependency(fromUri: String, typeFqName: String) { - addDependency(fromUri, typeFqName) - } - - fun clearDependencies(uri: String) { - val deps = dependsOn.remove(uri) - deps?.forEach { symbolFqName -> - dependedBy[symbolFqName]?.remove(uri) - } - } - - fun getDependencies(uri: String): Set { - return dependsOn[uri]?.toSet() ?: emptySet() - } - - fun getDependentFiles(symbolFqName: String): Set { - return dependedBy[symbolFqName]?.toSet() ?: emptySet() - } - - fun getFilesToInvalidate(changedUri: String, definedSymbols: Set): Set { - val filesToInvalidate = mutableSetOf() - - for (symbol in definedSymbols) { - val dependents = getDependentFiles(symbol) - filesToInvalidate.addAll(dependents) - } - - filesToInvalidate.remove(changedUri) - return filesToInvalidate - } - - fun hasAnyDependents(symbolFqName: String): Boolean { - return dependedBy[symbolFqName]?.isNotEmpty() == true - } - - fun trackDependenciesFromImports(uri: String, imports: List) { - clearDependencies(uri) - for (import in imports) { - addImportDependency(uri, import.fqName) - } - } - - fun trackDependenciesFromSymbolUsage(uri: String, usedSymbolFqNames: Set) { - for (fqName in usedSymbolFqNames) { - addDependency(uri, fqName) - } - } - - fun clear() { - dependsOn.clear() - dependedBy.clear() - } - - fun stats(): DependencyStats { - return DependencyStats( - totalFiles = dependsOn.size, - totalSymbols = dependedBy.size, - totalDependencies = dependsOn.values.sumOf { it.size } - ) - } -} - -data class ImportInfo( - val fqName: String, - val alias: String? = null, - val isStar: Boolean = false -) - -data class DependencyStats( - val totalFiles: Int, - val totalSymbols: Int, - val totalDependencies: Int -) diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ExtensionIndex.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ExtensionIndex.kt deleted file mode 100644 index 86e8b44039..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ExtensionIndex.kt +++ /dev/null @@ -1,222 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -import org.appdevforall.codeonthego.lsp.kotlin.types.ClassType -import org.appdevforall.codeonthego.lsp.kotlin.types.KotlinType - -/** - * Index of extension functions and properties by receiver type. - * - * ExtensionIndex provides efficient lookup of extensions applicable - * to a given receiver type, considering: - * - Exact type matches - * - Nullable type variants - * - Supertype extensions - * - * ## Usage - * - * ```kotlin - * val extensionIndex = ExtensionIndex() - * extensionIndex.add(listOfExtension) - * - * val stringExtensions = extensionIndex.findFor("kotlin.String") - * val iterableExtensions = extensionIndex.findFor("kotlin.collections.Iterable") - * ``` - */ -class ExtensionIndex { - - private val extensionsByReceiver = mutableMapOf>() - private val allExtensions = mutableListOf() - - /** - * Total number of extensions. - */ - val size: Int get() = allExtensions.size - - /** - * Whether the index is empty. - */ - val isEmpty: Boolean get() = allExtensions.isEmpty() - - /** - * All receiver types that have extensions. - */ - val receiverTypes: Set get() = extensionsByReceiver.keys.toSet() - - /** - * Adds an extension to the index. - * - * @param extension The extension symbol (must have receiverType) - */ - fun add(extension: IndexedSymbol) { - if (extension.receiverType == null) return - - allExtensions.add(extension) - val normalizedReceiver = normalizeType(extension.receiverType) - extensionsByReceiver.getOrPut(normalizedReceiver) { mutableListOf() }.add(extension) - } - - /** - * Adds multiple extensions. - */ - fun addAll(extensions: Iterable) { - extensions.forEach { add(it) } - } - - /** - * Removes an extension from the index. - */ - fun remove(extension: IndexedSymbol): Boolean { - if (extension.receiverType == null) return false - - allExtensions.remove(extension) - val normalizedReceiver = normalizeType(extension.receiverType) - extensionsByReceiver[normalizedReceiver]?.remove(extension) - if (extensionsByReceiver[normalizedReceiver]?.isEmpty() == true) { - extensionsByReceiver.remove(normalizedReceiver) - } - return true - } - - /** - * Clears all extensions. - */ - fun clear() { - extensionsByReceiver.clear() - allExtensions.clear() - } - - /** - * Finds extensions for an exact receiver type. - * - * @param receiverType The receiver type name - * @return Extensions declared for that exact type - */ - fun findExact(receiverType: String): List { - val normalized = normalizeType(receiverType) - return extensionsByReceiver[normalized]?.toList() ?: emptyList() - } - - /** - * Finds all extensions applicable to a receiver type. - * - * This includes: - * - Extensions on the exact type - * - Extensions on nullable variant - * - Extensions on supertypes - * - Extensions on Any/Any? (only if includeAnyExtensions is true) - * - * @param receiverType The receiver type name - * @param supertypes Optional list of supertype names to search - * @param includeAnyExtensions Whether to include extensions on Any/Any? - * @return All applicable extensions - */ - fun findFor( - receiverType: String, - supertypes: List = emptyList(), - includeAnyExtensions: Boolean = false - ): List { - val results = mutableListOf() - val normalized = normalizeType(receiverType) - - extensionsByReceiver[normalized]?.let { results.addAll(it) } - - val nullableVariant = if (normalized.endsWith("?")) { - normalized.dropLast(1) - } else { - "$normalized?" - } - extensionsByReceiver[nullableVariant]?.let { results.addAll(it) } - - for (supertype in supertypes) { - val normalizedSuper = normalizeType(supertype) - extensionsByReceiver[normalizedSuper]?.let { results.addAll(it) } - } - - if (includeAnyExtensions) { - extensionsByReceiver["Any"]?.let { results.addAll(it) } - extensionsByReceiver["Any?"]?.let { results.addAll(it) } - } - - return results.distinctBy { it.fqName } - } - - /** - * Finds extensions by name. - * - * @param name The extension name - * @return All extensions with that name - */ - fun findByName(name: String): List { - return allExtensions.filter { it.name == name } - } - - /** - * Finds extensions by name prefix. - * - * @param prefix The name prefix - * @param limit Maximum results (0 for unlimited) - * @return Matching extensions - */ - fun findByPrefix(prefix: String, limit: Int = 0): List { - val results = allExtensions.filter { it.name.startsWith(prefix, ignoreCase = true) } - return if (limit > 0) results.take(limit) else results - } - - /** - * Gets all extensions as a list. - */ - fun getAll(): List = allExtensions.toList() - - /** - * Gets extensions grouped by receiver type. - */ - fun getGroupedByReceiver(): Map> { - return extensionsByReceiver.mapValues { it.value.toList() } - } - - /** - * Merges another ExtensionIndex into this one. - */ - fun merge(other: ExtensionIndex) { - other.allExtensions.forEach { add(it) } - } - - /** - * Finds extensions applicable to a KotlinType. - */ - fun findForType(type: KotlinType): List { - val typeName = when (type) { - is ClassType -> type.fqName - else -> type.render() - } - - val supertypes = when (type) { - is ClassType -> type.symbol?.superTypes?.map { it.render() } ?: emptyList() - else -> emptyList() - } - - return findFor(typeName, supertypes) - } - - private fun normalizeType(typeName: String): String { - return typeName - .replace(Regex("<.*>"), "") - .trim() - } - - override fun toString(): String { - return "ExtensionIndex(receivers=${extensionsByReceiver.size}, extensions=$size)" - } - - companion object { - /** - * Creates an ExtensionIndex from a list of symbols. - * Only extensions are added. - */ - fun fromSymbols(symbols: Iterable): ExtensionIndex { - val index = ExtensionIndex() - symbols.filter { it.isExtension }.forEach { index.add(it) } - return index - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/FileIndex.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/FileIndex.kt deleted file mode 100644 index 8738ae0330..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/FileIndex.kt +++ /dev/null @@ -1,260 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.PropertySymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeAliasSymbol - -/** - * Per-file symbol index. - * - * FileIndex holds all symbols from a single Kotlin source file. - * It supports efficient lookup by name and provides incremental updates - * when a file is modified. - * - * ## Usage - * - * ```kotlin - * val fileIndex = FileIndex.fromSymbolTable(symbolTable) - * val classes = fileIndex.getAllClasses() - * val functions = fileIndex.findBySimpleName("onCreate") - * ``` - * - * @property filePath Path to the source file - * @property packageName The package declared in this file - * @property lastModified Timestamp of last file modification - */ -class FileIndex( - val filePath: String, - val packageName: String, - val lastModified: Long = System.currentTimeMillis() -) : MutableSymbolIndex { - - private val symbolsByFqName = mutableMapOf() - private val symbolsByName = mutableMapOf>() - - override val size: Int get() = symbolsByFqName.size - - override fun findByFqName(fqName: String): IndexedSymbol? { - return symbolsByFqName[fqName] - } - - override fun findBySimpleName(name: String): List { - return symbolsByName[name]?.toList() ?: emptyList() - } - - override fun findByPackage(packageName: String): List { - if (packageName != this.packageName) { - return emptyList() - } - return symbolsByFqName.values.filter { it.isTopLevel } - } - - override fun findByPrefix(prefix: String, limit: Int): List { - val results = symbolsByName.keys - .filter { it.startsWith(prefix, ignoreCase = true) } - .flatMap { symbolsByName[it] ?: emptyList() } - - return if (limit > 0) results.take(limit) else results - } - - override fun getAllClasses(): List { - return symbolsByFqName.values.filter { it.kind.isClass } - } - - override fun getAllFunctions(): List { - return symbolsByFqName.values.filter { it.kind == IndexedSymbolKind.FUNCTION } - } - - override fun getAllProperties(): List { - return symbolsByFqName.values.filter { it.kind == IndexedSymbolKind.PROPERTY } - } - - override fun add(symbol: IndexedSymbol) { - symbolsByFqName[symbol.fqName] = symbol - symbolsByName.getOrPut(symbol.name) { mutableListOf() }.add(symbol) - } - - override fun remove(fqName: String): Boolean { - val symbol = symbolsByFqName.remove(fqName) ?: return false - symbolsByName[symbol.name]?.remove(symbol) - if (symbolsByName[symbol.name]?.isEmpty() == true) { - symbolsByName.remove(symbol.name) - } - return true - } - - override fun clear() { - symbolsByFqName.clear() - symbolsByName.clear() - } - - /** - * Gets all top-level symbols. - */ - fun getTopLevelSymbols(): List { - return symbolsByFqName.values.filter { it.isTopLevel } - } - - fun findMembers(classFqName: String): List { - return symbolsByFqName.values.filter { it.containingClass == classFqName } - } - - /** - * Gets all extension symbols. - */ - fun getExtensions(): List { - return symbolsByFqName.values.filter { it.isExtension } - } - - /** - * Gets extensions for a specific receiver type. - */ - fun getExtensionsFor(receiverType: String): List { - return symbolsByFqName.values.filter { it.receiverType == receiverType } - } - - /** - * Exports to IndexData for serialization. - */ - fun toIndexData(): IndexData { - return IndexData.fromSymbols(symbolsByFqName.values.toList()) - } - - override fun toString(): String { - return "FileIndex($filePath, package=$packageName, symbols=$size)" - } - - companion object { - /** - * Creates a FileIndex from a SymbolTable. - */ - fun fromSymbolTable(symbolTable: SymbolTable): FileIndex { - val index = FileIndex( - filePath = symbolTable.filePath, - packageName = symbolTable.packageName - ) - - indexSymbols(symbolTable.topLevelSymbols, index, symbolTable.packageName) - - return index - } - - private fun indexSymbols( - symbols: List, - index: FileIndex, - packageName: String, - containingClass: String? = null - ) { - for (symbol in symbols) { - val indexed = symbolToIndexed(symbol, packageName, containingClass, index.filePath) - if (indexed != null) { - index.add(indexed) - } - - if (symbol is ClassSymbol && symbol.memberScope != null) { - indexSymbols( - symbol.members, - index, - packageName, - symbol.qualifiedName - ) - } - } - } - - private fun symbolToIndexed( - symbol: Symbol, - packageName: String, - containingClass: String?, - filePath: String - ): IndexedSymbol? { - val nameRange = symbol.location.nameRange - val hasLocation = !symbol.location.isSynthetic - - return when (symbol) { - is ClassSymbol -> IndexedSymbol( - name = symbol.name, - fqName = symbol.qualifiedName, - kind = IndexedSymbolKind.fromClassKind(symbol.kind), - packageName = packageName, - containingClass = containingClass, - visibility = symbol.visibility, - typeParameters = symbol.typeParameters.map { it.name }, - superTypes = symbol.superTypes.map { it.render() }, - filePath = filePath, - startLine = if (hasLocation) nameRange.startLine else null, - startColumn = if (hasLocation) nameRange.startColumn else null, - endLine = if (hasLocation) nameRange.endLine else null, - endColumn = if (hasLocation) nameRange.endColumn else null - ) - is FunctionSymbol -> IndexedSymbol( - name = symbol.name, - fqName = symbol.qualifiedName, - kind = if (symbol.isConstructor) IndexedSymbolKind.CONSTRUCTOR else IndexedSymbolKind.FUNCTION, - packageName = packageName, - containingClass = containingClass, - visibility = symbol.visibility, - typeParameters = symbol.typeParameters.map { it.name }, - parameters = symbol.parameters.map { param -> - IndexedParameter( - name = param.name, - type = param.type?.render() ?: "Any", - hasDefault = param.hasDefaultValue, - isVararg = param.isVararg - ) - }, - returnType = symbol.returnType?.render(), - receiverType = symbol.receiverType?.render(), - filePath = filePath, - startLine = if (hasLocation) nameRange.startLine else null, - startColumn = if (hasLocation) nameRange.startColumn else null, - endLine = if (hasLocation) nameRange.endLine else null, - endColumn = if (hasLocation) nameRange.endColumn else null - ) - is PropertySymbol -> IndexedSymbol( - name = symbol.name, - fqName = symbol.qualifiedName, - kind = IndexedSymbolKind.PROPERTY, - packageName = packageName, - containingClass = containingClass, - visibility = symbol.visibility, - returnType = symbol.type?.render(), - receiverType = symbol.receiverType?.render(), - filePath = filePath, - startLine = if (hasLocation) nameRange.startLine else null, - startColumn = if (hasLocation) nameRange.startColumn else null, - endLine = if (hasLocation) nameRange.endLine else null, - endColumn = if (hasLocation) nameRange.endColumn else null - ) - is TypeAliasSymbol -> IndexedSymbol( - name = symbol.name, - fqName = symbol.qualifiedName, - kind = IndexedSymbolKind.TYPE_ALIAS, - packageName = packageName, - containingClass = containingClass, - visibility = symbol.visibility, - typeParameters = symbol.typeParameters.map { it.name }, - returnType = symbol.underlyingType?.render(), - filePath = filePath, - startLine = if (hasLocation) nameRange.startLine else null, - startColumn = if (hasLocation) nameRange.startColumn else null, - endLine = if (hasLocation) nameRange.endLine else null, - endColumn = if (hasLocation) nameRange.endColumn else null - ) - else -> null - } - } - - /** - * Creates a FileIndex from IndexData. - */ - fun fromIndexData(data: IndexData, filePath: String, packageName: String): FileIndex { - val index = FileIndex(filePath, packageName) - data.toIndexedSymbols().forEach { index.add(it) } - return index - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/IndexEntry.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/IndexEntry.kt deleted file mode 100644 index ce6f753881..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/IndexEntry.kt +++ /dev/null @@ -1,357 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -import kotlinx.serialization.Serializable -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Visibility - -/** - * Serializable index entry for persistent storage. - * - * IndexEntry is the JSON-serializable form of symbol index data. - * It's used for: - * - Saving/loading project indexes - * - Distributing stdlib index in app assets - * - Caching index data - * - * ## JSON Format - * - * ```json - * { - * "name": "String", - * "fqName": "kotlin.String", - * "kind": "CLASS", - * "pkg": "kotlin", - * "sig": "class String : Comparable, CharSequence", - * "vis": "PUBLIC", - * "params": [], - * "typeParams": [], - * "ret": null, - * "recv": null, - * "supers": ["kotlin.Comparable", "kotlin.CharSequence"], - * "dep": false - * } - * ``` - */ -@Serializable -data class IndexEntry( - val name: String, - val fqName: String, - val kind: String, - val pkg: String, - val container: String? = null, - val sig: String = "", - val vis: String = "PUBLIC", - val typeParams: List = emptyList(), - val params: List = emptyList(), - val ret: String? = null, - val recv: String? = null, - val supers: List = emptyList(), - val file: String? = null, - val dep: Boolean = false, - val depMsg: String? = null -) { - /** - * Converts to an IndexedSymbol. - */ - fun toIndexedSymbol(): IndexedSymbol { - return IndexedSymbol( - name = name, - fqName = fqName, - kind = parseKind(kind), - packageName = pkg, - containingClass = container, - visibility = parseVisibility(vis), - signature = sig, - typeParameters = typeParams, - parameters = params.map { it.toIndexedParameter() }, - returnType = ret, - receiverType = recv, - superTypes = supers, - filePath = file, - deprecated = dep, - deprecationMessage = depMsg - ) - } - - private fun parseKind(kind: String): IndexedSymbolKind { - return try { - IndexedSymbolKind.valueOf(kind) - } catch (e: IllegalArgumentException) { - IndexedSymbolKind.CLASS - } - } - - private fun parseVisibility(vis: String): Visibility { - return try { - Visibility.valueOf(vis) - } catch (e: IllegalArgumentException) { - Visibility.PUBLIC - } - } - - companion object { - /** - * Creates an IndexEntry from an IndexedSymbol. - */ - fun fromIndexedSymbol(symbol: IndexedSymbol): IndexEntry { - return IndexEntry( - name = symbol.name, - fqName = symbol.fqName, - kind = symbol.kind.name, - pkg = symbol.packageName, - container = symbol.containingClass, - sig = symbol.signature, - vis = symbol.visibility.name, - typeParams = symbol.typeParameters, - params = symbol.parameters.map { ParamEntry.fromIndexedParameter(it) }, - ret = symbol.returnType, - recv = symbol.receiverType, - supers = symbol.superTypes, - file = symbol.filePath, - dep = symbol.deprecated, - depMsg = symbol.deprecationMessage - ) - } - } -} - -/** - * Serializable parameter entry. - */ -@Serializable -data class ParamEntry( - val name: String, - val type: String, - val def: Boolean = false, - val vararg: Boolean = false -) { - fun toIndexedParameter(): IndexedParameter { - return IndexedParameter( - name = name, - type = type, - hasDefault = def, - isVararg = vararg - ) - } - - companion object { - fun fromIndexedParameter(param: IndexedParameter): ParamEntry { - return ParamEntry( - name = param.name, - type = param.type, - def = param.hasDefault, - vararg = param.isVararg - ) - } - } -} - -/** - * Complete index data for serialization. - * - * Contains all symbols organized by category for efficient loading. - */ -@Serializable -data class IndexData( - val version: String = "1.0", - val kotlinVersion: String = "", - val generatedAt: Long = System.currentTimeMillis(), - val classes: List = emptyList(), - val functions: List = emptyList(), - val properties: List = emptyList(), - val typeAliases: List = emptyList(), - val extensions: List = emptyList() -) { - /** - * Total number of entries. - */ - val totalCount: Int get() = classes.size + functions.size + properties.size + - typeAliases.size + extensions.size - - /** - * Gets all entries as a single list. - */ - fun allEntries(): List { - return classes + functions + properties + typeAliases + extensions - } - - /** - * Converts all entries to IndexedSymbols. - */ - fun toIndexedSymbols(): List { - return allEntries().map { it.toIndexedSymbol() } - } - - companion object { - val EMPTY = IndexData() - - /** - * Creates IndexData from a list of IndexedSymbols. - */ - fun fromSymbols( - symbols: List, - version: String = "1.0", - kotlinVersion: String = "" - ): IndexData { - val classes = mutableListOf() - val functions = mutableListOf() - val properties = mutableListOf() - val typeAliases = mutableListOf() - val extensions = mutableListOf() - - for (symbol in symbols) { - val entry = IndexEntry.fromIndexedSymbol(symbol) - when { - symbol.isExtension -> extensions.add(entry) - symbol.kind == IndexedSymbolKind.TYPE_ALIAS -> typeAliases.add(entry) - symbol.kind == IndexedSymbolKind.FUNCTION || - symbol.kind == IndexedSymbolKind.CONSTRUCTOR -> functions.add(entry) - symbol.kind == IndexedSymbolKind.PROPERTY -> properties.add(entry) - symbol.kind.isClass -> classes.add(entry) - else -> classes.add(entry) - } - } - - return IndexData( - version = version, - kotlinVersion = kotlinVersion, - classes = classes, - functions = functions, - properties = properties, - typeAliases = typeAliases, - extensions = extensions - ) - } - } -} - -/** - * Class member index entry. - */ -@Serializable -data class MemberEntry( - val name: String, - val kind: String, - val sig: String = "", - val params: List = emptyList(), - val ret: String? = null, - val vis: String = "PUBLIC", - val dep: Boolean = false -) - -/** - * Full class information for stdlib index. - */ -@Serializable -data class ClassEntry( - val fqName: String, - val kind: String, - val typeParams: List = emptyList(), - val supers: List = emptyList(), - val members: List = emptyList(), - val companions: List = emptyList(), - val nested: List = emptyList(), - val dep: Boolean = false, - val depMsg: String? = null -) { - val simpleName: String get() = fqName.substringAfterLast('.') - val packageName: String get() = fqName.substringBeforeLast('.', "") - - /** - * Converts to IndexedSymbols (class + members). - */ - fun toIndexedSymbols(): List { - val result = mutableListOf() - - result.add(IndexedSymbol( - name = simpleName, - fqName = fqName, - kind = parseKind(kind), - packageName = packageName, - typeParameters = typeParams, - superTypes = supers, - deprecated = dep, - deprecationMessage = depMsg - )) - - for (member in members) { - result.add(IndexedSymbol( - name = member.name, - fqName = "$fqName.${member.name}", - kind = parseKind(member.kind), - packageName = packageName, - containingClass = fqName, - signature = member.sig, - parameters = member.params.map { it.toIndexedParameter() }, - returnType = member.ret, - visibility = parseVisibility(member.vis), - deprecated = member.dep - )) - } - - return result - } - - private fun parseKind(kind: String): IndexedSymbolKind { - return try { - IndexedSymbolKind.valueOf(kind) - } catch (e: IllegalArgumentException) { - IndexedSymbolKind.CLASS - } - } - - private fun parseVisibility(vis: String): Visibility { - return try { - Visibility.valueOf(vis) - } catch (e: IllegalArgumentException) { - Visibility.PUBLIC - } - } -} - -/** - * Stdlib index format. - */ -@Serializable -data class StdlibIndexData( - val version: String, - val kotlinVersion: String, - val generatedAt: Long = System.currentTimeMillis(), - val classes: Map = emptyMap(), - val topLevelFunctions: List = emptyList(), - val topLevelProperties: List = emptyList(), - val extensions: Map> = emptyMap(), - val typeAliases: List = emptyList() -) { - /** - * Total symbol count. - */ - val totalCount: Int get() { - var count = classes.size + topLevelFunctions.size + topLevelProperties.size + typeAliases.size - classes.values.forEach { count += it.members.size } - extensions.values.forEach { count += it.size } - return count - } - - /** - * Gets all top-level symbols as IndexedSymbols. - */ - fun toIndexedSymbols(): List { - val result = mutableListOf() - - classes.values.forEach { classEntry -> - result.addAll(classEntry.toIndexedSymbols()) - } - - topLevelFunctions.forEach { result.add(it.toIndexedSymbol()) } - topLevelProperties.forEach { result.add(it.toIndexedSymbol()) } - typeAliases.forEach { result.add(it.toIndexedSymbol()) } - - extensions.values.flatten().forEach { result.add(it.toIndexedSymbol()) } - - return result - } - - companion object { - val EMPTY = StdlibIndexData(version = "1.0", kotlinVersion = "") - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/PackageIndex.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/PackageIndex.kt deleted file mode 100644 index edcc727788..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/PackageIndex.kt +++ /dev/null @@ -1,144 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -/** - * Index organized by package name. - * - * PackageIndex provides efficient lookup of symbols within a package - * or across all packages with a common prefix. - * - * ## Usage - * - * ```kotlin - * val packageIndex = PackageIndex() - * packageIndex.add(symbol) - * - * val kotlinSymbols = packageIndex.getPackage("kotlin") - * val collectionsSymbols = packageIndex.getPackage("kotlin.collections") - * val allKotlin = packageIndex.getPackagesWithPrefix("kotlin") - * ``` - */ -class PackageIndex : MutableSymbolIndex { - - private val packageMap = mutableMapOf>() - private val fqNameMap = mutableMapOf() - - override val size: Int get() = fqNameMap.size - - /** - * All package names in the index. - */ - val packageNames: Set get() = packageMap.keys.toSet() - - /** - * Number of packages in the index. - */ - val packageCount: Int get() = packageMap.size - - override fun findByFqName(fqName: String): IndexedSymbol? { - return fqNameMap[fqName] - } - - override fun findBySimpleName(name: String): List { - return fqNameMap.values.filter { it.name == name } - } - - override fun findByPackage(packageName: String): List { - return packageMap[packageName]?.toList() ?: emptyList() - } - - override fun findByPrefix(prefix: String, limit: Int): List { - val results = fqNameMap.values.filter { - it.name.startsWith(prefix, ignoreCase = true) - } - return if (limit > 0) results.take(limit) else results - } - - override fun getAllClasses(): List { - return fqNameMap.values.filter { it.kind.isClass } - } - - override fun getAllFunctions(): List { - return fqNameMap.values.filter { it.kind == IndexedSymbolKind.FUNCTION } - } - - override fun getAllProperties(): List { - return fqNameMap.values.filter { it.kind == IndexedSymbolKind.PROPERTY } - } - - override fun add(symbol: IndexedSymbol) { - fqNameMap[symbol.fqName] = symbol - packageMap.getOrPut(symbol.packageName) { mutableListOf() }.add(symbol) - } - - override fun remove(fqName: String): Boolean { - val symbol = fqNameMap.remove(fqName) ?: return false - packageMap[symbol.packageName]?.remove(symbol) - if (packageMap[symbol.packageName]?.isEmpty() == true) { - packageMap.remove(symbol.packageName) - } - return true - } - - override fun clear() { - fqNameMap.clear() - packageMap.clear() - } - - /** - * Gets all symbols from packages starting with a prefix. - * - * @param prefix Package prefix (e.g., "kotlin" matches "kotlin", "kotlin.collections") - * @return All symbols in matching packages - */ - fun getPackagesWithPrefix(prefix: String): List { - return packageMap.entries - .filter { it.key.startsWith(prefix) } - .flatMap { it.value } - } - - /** - * Gets all subpackages of a package. - * - * @param parentPackage The parent package name - * @return Names of direct child packages - */ - fun getSubpackages(parentPackage: String): List { - val prefix = if (parentPackage.isEmpty()) "" else "$parentPackage." - return packageMap.keys - .filter { it.startsWith(prefix) && it != parentPackage } - .map { name -> - val rest = name.removePrefix(prefix) - val firstDot = rest.indexOf('.') - if (firstDot > 0) rest.substring(0, firstDot) else rest - } - .distinct() - .map { if (parentPackage.isEmpty()) it else "$parentPackage.$it" } - } - - /** - * Gets root packages (packages with no parent). - */ - fun getRootPackages(): List { - return packageMap.keys - .map { it.substringBefore('.') } - .distinct() - } - - /** - * Checks if a package exists. - */ - fun hasPackage(packageName: String): Boolean { - return packageMap.containsKey(packageName) - } - - /** - * Merges another PackageIndex into this one. - */ - fun merge(other: PackageIndex) { - other.fqNameMap.values.forEach { add(it) } - } - - override fun toString(): String { - return "PackageIndex(packages=$packageCount, symbols=$size)" - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ProjectIndex.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ProjectIndex.kt deleted file mode 100644 index 28996f924f..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/ProjectIndex.kt +++ /dev/null @@ -1,454 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -import java.util.concurrent.ConcurrentHashMap - -/** - * Project-wide symbol index aggregating all file indexes. - * - * ProjectIndex provides unified symbol lookup across: - * - All indexed project source files - * - Standard library symbols (via StdlibIndex) - * - Dependencies (if indexed) - * - * ## Thread Safety - * - * ProjectIndex is thread-safe for concurrent read/write operations. - * File indexes can be updated while queries are being processed. - * - * ## Usage - * - * ```kotlin - * val projectIndex = ProjectIndex() - * projectIndex.updateFile(fileIndex) - * - * val stringClass = projectIndex.findByFqName("kotlin.String") - * val completions = projectIndex.findByPrefix("str", limit = 50) - * ``` - */ -class ProjectIndex : SymbolIndex { - - private val fileIndexes = ConcurrentHashMap() - private val packageIndex = PackageIndex() - private val extensionIndex = ExtensionIndex() - - @Volatile - private var stdlibIndex: StdlibIndex? = null - - @Volatile - private var classpathIndex: ClasspathIndex? = null - - override val size: Int - get() = fileIndexes.values.sumOf { it.size } + (stdlibIndex?.size ?: 0) + (classpathIndex?.size ?: 0) - - val fileCount: Int get() = fileIndexes.size - - val packageNames: Set - get() = packageIndex.packageNames + (stdlibIndex?.packageNames ?: emptySet()) + (classpathIndex?.packageNames ?: emptySet()) - - override fun findByFqName(fqName: String): IndexedSymbol? { - for (fileIndex in fileIndexes.values) { - fileIndex.findByFqName(fqName)?.let { return it } - } - - stdlibIndex?.findByFqName(fqName)?.let { return it } - - return classpathIndex?.findByFqName(fqName) - } - - override fun findBySimpleName(name: String): List { - val results = mutableListOf() - - for (fileIndex in fileIndexes.values) { - results.addAll(fileIndex.findBySimpleName(name)) - } - - stdlibIndex?.findBySimpleName(name)?.let { results.addAll(it) } - classpathIndex?.findBySimpleName(name)?.let { results.addAll(it) } - - return results.distinctBy { it.fqName } - } - - fun findInProjectFiles(name: String): List { - val results = mutableListOf() - for (fileIndex in fileIndexes.values) { - results.addAll(fileIndex.findBySimpleName(name)) - } - return results - } - - override fun findByPackage(packageName: String): List { - val results = mutableListOf() - - results.addAll(packageIndex.findByPackage(packageName)) - stdlibIndex?.findByPackage(packageName)?.let { results.addAll(it) } - classpathIndex?.findByPackage(packageName)?.let { results.addAll(it) } - - return results.distinctBy { it.fqName } - } - - override fun findByPrefix(prefix: String, limit: Int): List { - val results = mutableListOf() - - for (fileIndex in fileIndexes.values) { - results.addAll(fileIndex.findByPrefix(prefix, 0)) - } - - stdlibIndex?.findByPrefix(prefix, 0)?.let { results.addAll(it) } - classpathIndex?.findByPrefix(prefix, 0)?.let { results.addAll(it) } - - val distinct = results.distinctBy { it.fqName } - return if (limit > 0) distinct.take(limit) else distinct - } - - override fun getAllClasses(): List { - val results = mutableListOf() - - for (fileIndex in fileIndexes.values) { - results.addAll(fileIndex.getAllClasses()) - } - - stdlibIndex?.getAllClasses()?.let { results.addAll(it) } - classpathIndex?.getAllClasses()?.let { results.addAll(it) } - - return results.distinctBy { it.fqName } - } - - override fun getAllFunctions(): List { - val results = mutableListOf() - - for (fileIndex in fileIndexes.values) { - results.addAll(fileIndex.getAllFunctions()) - } - - stdlibIndex?.getAllFunctions()?.let { results.addAll(it) } - classpathIndex?.getAllFunctions()?.let { results.addAll(it) } - - return results.distinctBy { it.fqName } - } - - override fun getAllProperties(): List { - val results = mutableListOf() - - for (fileIndex in fileIndexes.values) { - results.addAll(fileIndex.getAllProperties()) - } - - stdlibIndex?.getAllProperties()?.let { results.addAll(it) } - classpathIndex?.getAllProperties()?.let { results.addAll(it) } - - return results.distinctBy { it.fqName } - } - - /** - * Updates or adds a file index. - * - * @param fileIndex The file index to add/update - */ - fun updateFile(fileIndex: FileIndex) { - val oldIndex = fileIndexes.put(fileIndex.filePath, fileIndex) - - if (oldIndex != null) { - removeFromPackageIndex(oldIndex) - removeFromExtensionIndex(oldIndex) - } - - addToPackageIndex(fileIndex) - addToExtensionIndex(fileIndex) - } - - /** - * Removes a file from the index. - * - * @param filePath Path of the file to remove - * @return true if the file was indexed and removed - */ - fun removeFile(filePath: String): Boolean { - val fileIndex = fileIndexes.remove(filePath) ?: return false - - removeFromPackageIndex(fileIndex) - removeFromExtensionIndex(fileIndex) - - return true - } - - /** - * Gets a file index by path. - */ - fun getFileIndex(filePath: String): FileIndex? { - return fileIndexes[filePath] - } - - /** - * Gets all indexed file paths. - */ - fun getIndexedFiles(): Set { - return fileIndexes.keys.toSet() - } - - /** - * Sets the stdlib index. - */ - fun setStdlibIndex(index: StdlibIndex) { - stdlibIndex = index - } - - /** - * Gets the stdlib index if loaded. - */ - fun getStdlibIndex(): StdlibIndex? { - return stdlibIndex - } - - /** - * Sets the classpath index. - */ - fun setClasspathIndex(index: ClasspathIndex) { - classpathIndex = index - } - - /** - * Gets the classpath index if loaded. - */ - fun getClasspathIndex(): ClasspathIndex? { - return classpathIndex - } - - /** - * Finds extensions applicable to a receiver type. - * - * @param receiverType The receiver type name - * @param supertypes Optional supertypes to include - * @param includeAnyExtensions Whether to include extensions on Any/Any? - * @return All applicable extensions from project and stdlib - */ - fun findExtensions( - receiverType: String, - supertypes: List = emptyList(), - includeAnyExtensions: Boolean = false - ): List { - val results = mutableListOf() - - results.addAll(extensionIndex.findFor(receiverType, supertypes, includeAnyExtensions)) - stdlibIndex?.findExtensions(receiverType, supertypes, includeAnyExtensions)?.let { results.addAll(it) } - - classpathIndex?.let { cpIndex -> - results.addAll(cpIndex.findExtensionsFor(receiverType)) - supertypes.forEach { st -> results.addAll(cpIndex.findExtensionsFor(st)) } - } - - return results.distinctBy { it.fqName } - } - - /** - * Finds symbols visible from a given file. - * - * Includes: - * - Symbols in the same file - * - Symbols in the same package - * - Public symbols from other packages - * - Stdlib symbols - * - * @param filePath The file path for context - * @param prefix Optional name prefix filter - * @param limit Maximum results - * @return Visible symbols - */ - fun findVisibleFrom( - filePath: String, - prefix: String = "", - limit: Int = 0 - ): List { - val results = mutableListOf() - - val currentFile = fileIndexes[filePath] - val currentPackage = currentFile?.packageName ?: "" - - currentFile?.findByPrefix(prefix, 0)?.let { results.addAll(it) } - - if (currentPackage.isNotEmpty()) { - packageIndex.findByPackage(currentPackage) - .filter { prefix.isEmpty() || it.name.startsWith(prefix, ignoreCase = true) } - .filter { it.filePath != filePath } - .let { results.addAll(it) } - } - - for (fileIndex in fileIndexes.values) { - if (fileIndex.filePath == filePath) continue - if (fileIndex.packageName == currentPackage) continue - - fileIndex.findByPrefix(prefix, 0) - .filter { it.isPublicApi && it.isTopLevel } - .let { results.addAll(it) } - } - - stdlibIndex?.findByPrefix(prefix, 0) - ?.filter { it.isPublicApi } - ?.let { results.addAll(it) } - - classpathIndex?.findByPrefix(prefix, 0) - ?.filter { it.isPublicApi } - ?.let { results.addAll(it) } - - val distinct = results.distinctBy { it.fqName } - return if (limit > 0) distinct.take(limit) else distinct - } - - /** - * Executes a query against the index. - * - * @param query The query parameters - * @return Matching symbols - */ - fun query(query: IndexQuery): List { - var results = when { - query.namePrefix != null -> findByPrefix(query.namePrefix, 0) - query.packageName != null -> findByPackage(query.packageName) - query.extensionReceiverType != null -> findExtensions(query.extensionReceiverType) - else -> getAllSymbols() - } - - if (query.kinds != null) { - results = results.filter { it.kind in query.kinds } - } - - if (!query.includeDeprecated) { - results = results.filter { !it.deprecated } - } - - if (!query.includeInternal) { - results = results.filter { it.isPublicApi } - } - - return if (query.limit > 0) results.take(query.limit) else results - } - - /** - * Gets all symbols in the index. - */ - fun getAllSymbols(): List { - val results = mutableListOf() - - for (fileIndex in fileIndexes.values) { - results.addAll(fileIndex.getTopLevelSymbols()) - } - - stdlibIndex?.getAllSymbols()?.let { results.addAll(it) } - classpathIndex?.getAllSymbols()?.let { results.addAll(it) } - - return results.distinctBy { it.fqName } - } - - /** - * Clears all indexed data. - */ - fun clear() { - fileIndexes.clear() - packageIndex.clear() - extensionIndex.clear() - stdlibIndex = null - classpathIndex = null - } - - /** - * Creates an IndexData snapshot of all project symbols. - */ - fun toIndexData(): IndexData { - val allSymbols = mutableListOf() - - for (fileIndex in fileIndexes.values) { - allSymbols.addAll(fileIndex.getTopLevelSymbols()) - } - - return IndexData.fromSymbols(allSymbols) - } - - private fun addToPackageIndex(fileIndex: FileIndex) { - for (symbol in fileIndex.getTopLevelSymbols()) { - packageIndex.add(symbol) - } - } - - private fun removeFromPackageIndex(fileIndex: FileIndex) { - for (symbol in fileIndex.getTopLevelSymbols()) { - packageIndex.remove(symbol.fqName) - } - } - - private fun addToExtensionIndex(fileIndex: FileIndex) { - extensionIndex.addAll(fileIndex.getExtensions()) - } - - private fun removeFromExtensionIndex(fileIndex: FileIndex) { - for (extension in fileIndex.getExtensions()) { - extensionIndex.remove(extension) - } - } - - fun findMembersInProjectFiles(classFqName: String): List { - val results = mutableListOf() - for (fileIndex in fileIndexes.values) { - results.addAll(fileIndex.findMembers(classFqName)) - } - return results - } - - fun findByFqNameInProjectFiles(fqName: String): IndexedSymbol? { - for (fileIndex in fileIndexes.values) { - fileIndex.findByFqName(fqName)?.let { return it } - } - return null - } - - fun findSymbolReferences(fqName: String): List { - val references = mutableListOf() - - for ((filePath, fileIndex) in fileIndexes) { - val fileRefs = findReferencesInFile(fileIndex, fqName) - for (ref in fileRefs) { - references.add(SymbolReference( - filePath = filePath, - symbolFqName = fqName, - referenceFqName = ref.fqName, - referenceKind = ref.kind - )) - } - } - - return references - } - - private fun findReferencesInFile(fileIndex: FileIndex, targetFqName: String): List { - return fileIndex.getTopLevelSymbols().filter { symbol -> - when (symbol.kind) { - IndexedSymbolKind.FUNCTION, - IndexedSymbolKind.CONSTRUCTOR -> { - symbol.parameters.any { it.type == targetFqName || it.type.endsWith(".$targetFqName") } || - symbol.returnType == targetFqName || - symbol.receiverType == targetFqName - } - IndexedSymbolKind.PROPERTY -> { - symbol.returnType == targetFqName || - symbol.receiverType == targetFqName - } - IndexedSymbolKind.CLASS, - IndexedSymbolKind.INTERFACE, - IndexedSymbolKind.ENUM_CLASS, - IndexedSymbolKind.OBJECT -> { - symbol.superTypes.any { it == targetFqName || it.endsWith(".$targetFqName") } - } - else -> false - } - } - } - - override fun toString(): String { - return "ProjectIndex(files=$fileCount, symbols=$size, stdlib=${stdlibIndex != null})" - } -} - -data class SymbolReference( - val filePath: String, - val symbolFqName: String, - val referenceFqName: String, - val referenceKind: IndexedSymbolKind -) diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndex.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndex.kt deleted file mode 100644 index 028f33ba4a..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndex.kt +++ /dev/null @@ -1,259 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -/** - * Index of Kotlin standard library symbols. - * - * StdlibIndex provides fast lookup for all Kotlin stdlib types, - * functions, properties, and extensions. The index is loaded from - * a pre-generated JSON file (stdlib-index.json). - * - * ## Features - * - * - Fast O(1) class lookup by fully qualified name - * - Extension function lookup by receiver type - * - Prefix search for code completion - * - Package-based organization - * - * ## Usage - * - * ```kotlin - * val stdlibIndex = StdlibIndexLoader.loadFromAssets(context) - * val stringClass = stdlibIndex.findByFqName("kotlin.String") - * val stringExtensions = stdlibIndex.findExtensions("kotlin.String") - * ``` - * - * @property version The Kotlin stdlib version - * @property kotlinVersion The Kotlin compiler version - */ -class StdlibIndex( - val version: String, - val kotlinVersion: String -) : SymbolIndex { - - private val symbolsByFqName = mutableMapOf() - private val symbolsByName = mutableMapOf>() - private val symbolsByPackage = mutableMapOf>() - private val extensionsByReceiver = mutableMapOf>() - private val allSymbols = mutableListOf() - - override val size: Int get() = allSymbols.size - - val packageNames: Set get() = symbolsByPackage.keys.toSet() - - val extensionReceiverTypes: Set get() = extensionsByReceiver.keys.toSet() - - override fun findByFqName(fqName: String): IndexedSymbol? { - return symbolsByFqName[fqName] - } - - override fun findBySimpleName(name: String): List { - return symbolsByName[name]?.toList() ?: emptyList() - } - - override fun findByPackage(packageName: String): List { - return symbolsByPackage[packageName]?.toList() ?: emptyList() - } - - override fun findByPrefix(prefix: String, limit: Int): List { - val results = symbolsByName.keys - .filter { it.startsWith(prefix, ignoreCase = true) } - .flatMap { symbolsByName[it] ?: emptyList() } - - return if (limit > 0) results.take(limit) else results - } - - override fun getAllClasses(): List { - return allSymbols.filter { it.kind.isClass } - } - - override fun getAllFunctions(): List { - return allSymbols.filter { it.kind == IndexedSymbolKind.FUNCTION } - } - - override fun getAllProperties(): List { - return allSymbols.filter { it.kind == IndexedSymbolKind.PROPERTY } - } - - /** - * Gets all symbols. - */ - fun getAllSymbols(): List { - return allSymbols.toList() - } - - /** - * Finds extensions for a receiver type. - * - * @param receiverType The receiver type name (e.g., "kotlin.String") - * @param supertypes Optional supertypes to include - * @param includeAnyExtensions Whether to include extensions on Any/Any? - * @return Extensions applicable to the receiver - */ - fun findExtensions( - receiverType: String, - supertypes: List = emptyList(), - includeAnyExtensions: Boolean = false - ): List { - val results = mutableListOf() - - val normalized = normalizeType(receiverType) - extensionsByReceiver[normalized]?.let { results.addAll(it) } - - val nullableVariant = if (normalized.endsWith("?")) { - normalized.dropLast(1) - } else { - "$normalized?" - } - extensionsByReceiver[nullableVariant]?.let { results.addAll(it) } - - for (supertype in supertypes) { - val normalizedSuper = normalizeType(supertype) - extensionsByReceiver[normalizedSuper]?.let { results.addAll(it) } - } - - if (includeAnyExtensions) { - extensionsByReceiver["Any"]?.let { results.addAll(it) } - extensionsByReceiver["Any?"]?.let { results.addAll(it) } - } - - return results.distinctBy { it.fqName } - } - - /** - * Finds extensions by name. - */ - fun findExtensionsByName(name: String): List { - return allSymbols.filter { it.isExtension && it.name == name } - } - - /** - * Finds members of a class. - * - * @param classFqName Fully qualified class name - * @return Members of the class - */ - fun findMembers(classFqName: String): List { - return allSymbols.filter { it.containingClass == classFqName } - } - - /** - * Checks if a package exists in the stdlib. - */ - fun hasPackage(packageName: String): Boolean { - return symbolsByPackage.containsKey(packageName) - } - - /** - * Gets subpackages of a parent package. - */ - fun getSubpackages(parentPackage: String): List { - val prefix = if (parentPackage.isEmpty()) "" else "$parentPackage." - return symbolsByPackage.keys - .filter { it.startsWith(prefix) && it != parentPackage } - .map { name -> - val rest = name.removePrefix(prefix) - val firstDot = rest.indexOf('.') - if (firstDot > 0) rest.substring(0, firstDot) else rest - } - .distinct() - .map { if (parentPackage.isEmpty()) it else "$parentPackage.$it" } - } - - /** - * Gets commonly used symbols for quick completion. - */ - fun getCommonSymbols(): List { - val commonNames = setOf( - "kotlin.String", "kotlin.Int", "kotlin.Boolean", "kotlin.Long", - "kotlin.Double", "kotlin.Float", "kotlin.Any", "kotlin.Unit", - "kotlin.collections.List", "kotlin.collections.Map", "kotlin.collections.Set", - "kotlin.collections.MutableList", "kotlin.collections.MutableMap", - "kotlin.Pair", "kotlin.Triple" - ) - val commonFunctions = setOf( - "println", "print", "listOf", "mapOf", "setOf", - "mutableListOf", "mutableMapOf", "mutableSetOf", - "arrayOf", "emptyList", "emptyMap", "emptySet", - "to", "let", "run", "with", "apply", "also", - "takeIf", "takeUnless", "repeat", "require", "check" - ) - - val results = mutableListOf() - - for (fqName in commonNames) { - symbolsByFqName[fqName]?.let { results.add(it) } - } - - for (name in commonFunctions) { - symbolsByName[name]?.firstOrNull()?.let { results.add(it) } - } - - return results - } - - internal fun addSymbol(symbol: IndexedSymbol) { - allSymbols.add(symbol) - symbolsByFqName[symbol.fqName] = symbol - symbolsByName.getOrPut(symbol.name) { mutableListOf() }.add(symbol) - - if (symbol.isTopLevel) { - symbolsByPackage.getOrPut(symbol.packageName) { mutableListOf() }.add(symbol) - } - - if (symbol.isExtension && symbol.receiverType != null) { - val normalized = normalizeType(symbol.receiverType) - extensionsByReceiver.getOrPut(normalized) { mutableListOf() }.add(symbol) - } - } - - internal fun addAll(symbols: Iterable) { - symbols.forEach { addSymbol(it) } - } - - private fun normalizeType(typeName: String): String { - return typeName - .replace(Regex("<.*>"), "") - .trim() - } - - override fun toString(): String { - return "StdlibIndex(version=$version, kotlin=$kotlinVersion, symbols=$size)" - } - - companion object { - /** - * Creates an empty StdlibIndex. - */ - fun empty(): StdlibIndex { - return StdlibIndex(version = "0.0", kotlinVersion = "unknown") - } - - /** - * Creates a StdlibIndex from StdlibIndexData. - */ - fun fromData(data: StdlibIndexData): StdlibIndex { - val index = StdlibIndex( - version = data.version, - kotlinVersion = data.kotlinVersion - ) - - index.addAll(data.toIndexedSymbols()) - - return index - } - - /** - * Creates a StdlibIndex from IndexData. - */ - fun fromIndexData(data: IndexData): StdlibIndex { - val index = StdlibIndex( - version = data.version, - kotlinVersion = data.kotlinVersion - ) - - index.addAll(data.toIndexedSymbols()) - - return index - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndexLoader.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndexLoader.kt deleted file mode 100644 index b3eb94171e..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/StdlibIndexLoader.kt +++ /dev/null @@ -1,461 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -import kotlinx.serialization.json.Json -import java.io.InputStream -import java.io.Reader - -/** - * Loader for stdlib index from JSON files. - * - * StdlibIndexLoader handles deserialization of the pre-generated - * stdlib-index.json file into a usable StdlibIndex. - * - * ## File Formats - * - * Two formats are supported: - * 1. **IndexData** - Simple flat list of symbols - * 2. **StdlibIndexData** - Optimized format with class hierarchy - * - * ## Usage - * - * ```kotlin - * // From Android assets - * val index = StdlibIndexLoader.loadFromAssets(context.assets) - * - * // From InputStream - * val inputStream = File("stdlib-index.json").inputStream() - * val index = StdlibIndexLoader.loadFromStream(inputStream) - * - * // From JSON string - * val json = File("stdlib-index.json").readText() - * val index = StdlibIndexLoader.loadFromJson(json) - * ``` - */ -object StdlibIndexLoader { - - private val json = Json { - ignoreUnknownKeys = true - isLenient = true - coerceInputValues = true - } - - /** - * Loads StdlibIndex from a JSON string. - * - * Auto-detects format (IndexData or StdlibIndexData). - * - * @param jsonString The JSON content - * @return Loaded StdlibIndex - * @throws kotlinx.serialization.SerializationException on parse error - */ - fun loadFromJson(jsonString: String): StdlibIndex { - return if (jsonString.contains("\"classes\":")) { - if (jsonString.contains("\"members\":")) { - val data = json.decodeFromString(jsonString) - StdlibIndex.fromData(data) - } else { - val data = json.decodeFromString(jsonString) - StdlibIndex.fromIndexData(data) - } - } else { - StdlibIndex.empty() - } - } - - /** - * Loads StdlibIndex from an InputStream. - * - * @param inputStream The input stream (will be closed) - * @return Loaded StdlibIndex - */ - fun loadFromStream(inputStream: InputStream): StdlibIndex { - return inputStream.use { stream -> - val jsonString = stream.bufferedReader().readText() - loadFromJson(jsonString) - } - } - - /** - * Loads StdlibIndex from a Reader. - * - * @param reader The reader (will be closed) - * @return Loaded StdlibIndex - */ - fun loadFromReader(reader: Reader): StdlibIndex { - return reader.use { - val jsonString = it.readText() - loadFromJson(jsonString) - } - } - - /** - * Loads IndexData from JSON string. - * - * @param jsonString The JSON content - * @return Parsed IndexData - */ - fun parseIndexData(jsonString: String): IndexData { - return json.decodeFromString(jsonString) - } - - /** - * Loads StdlibIndexData from JSON string. - * - * @param jsonString The JSON content - * @return Parsed StdlibIndexData - */ - fun parseStdlibIndexData(jsonString: String): StdlibIndexData { - return json.decodeFromString(jsonString) - } - - /** - * Serializes IndexData to JSON string. - * - * @param data The index data - * @param pretty Whether to pretty-print - * @return JSON string - */ - fun toJson(data: IndexData, pretty: Boolean = false): String { - val encoder = if (pretty) { - Json { - prettyPrint = true - encodeDefaults = false - } - } else { - Json { - encodeDefaults = false - } - } - return encoder.encodeToString(IndexData.serializer(), data) - } - - /** - * Serializes StdlibIndexData to JSON string. - * - * @param data The stdlib index data - * @param pretty Whether to pretty-print - * @return JSON string - */ - fun toJson(data: StdlibIndexData, pretty: Boolean = false): String { - val encoder = if (pretty) { - Json { - prettyPrint = true - encodeDefaults = false - } - } else { - Json { - encodeDefaults = false - } - } - return encoder.encodeToString(StdlibIndexData.serializer(), data) - } - - /** - * Creates a minimal stdlib index with essential types. - * - * Useful for testing or when full index is unavailable. - * - * @return StdlibIndex with core types only - */ - fun createMinimalIndex(): StdlibIndex { - val index = StdlibIndex(version = "minimal", kotlinVersion = "2.0") - - addPrimitiveTypes(index) - addCoreClasses(index) - addCoreFunctions(index) - - return index - } - - private fun addPrimitiveTypes(index: StdlibIndex) { - val primitives = listOf( - "Int" to "32-bit signed integer", - "Long" to "64-bit signed integer", - "Short" to "16-bit signed integer", - "Byte" to "8-bit signed integer", - "Float" to "32-bit floating point", - "Double" to "64-bit floating point", - "Boolean" to "Boolean value (true/false)", - "Char" to "16-bit Unicode character" - ) - - for ((name, _) in primitives) { - index.addSymbol(IndexedSymbol( - name = name, - fqName = "kotlin.$name", - kind = IndexedSymbolKind.CLASS, - packageName = "kotlin", - superTypes = listOf("kotlin.Number", "kotlin.Comparable") - )) - } - } - - private fun addCoreClasses(index: StdlibIndex) { - index.addSymbol(IndexedSymbol( - name = "Any", - fqName = "kotlin.Any", - kind = IndexedSymbolKind.CLASS, - packageName = "kotlin" - )) - - index.addSymbol(IndexedSymbol( - name = "Nothing", - fqName = "kotlin.Nothing", - kind = IndexedSymbolKind.CLASS, - packageName = "kotlin" - )) - - index.addSymbol(IndexedSymbol( - name = "Unit", - fqName = "kotlin.Unit", - kind = IndexedSymbolKind.OBJECT, - packageName = "kotlin" - )) - - index.addSymbol(IndexedSymbol( - name = "String", - fqName = "kotlin.String", - kind = IndexedSymbolKind.CLASS, - packageName = "kotlin", - superTypes = listOf("kotlin.Comparable", "kotlin.CharSequence") - )) - - index.addSymbol(IndexedSymbol( - name = "CharSequence", - fqName = "kotlin.CharSequence", - kind = IndexedSymbolKind.INTERFACE, - packageName = "kotlin" - )) - - index.addSymbol(IndexedSymbol( - name = "Comparable", - fqName = "kotlin.Comparable", - kind = IndexedSymbolKind.INTERFACE, - packageName = "kotlin", - typeParameters = listOf("T") - )) - - index.addSymbol(IndexedSymbol( - name = "Number", - fqName = "kotlin.Number", - kind = IndexedSymbolKind.CLASS, - packageName = "kotlin" - )) - - index.addSymbol(IndexedSymbol( - name = "Throwable", - fqName = "kotlin.Throwable", - kind = IndexedSymbolKind.CLASS, - packageName = "kotlin" - )) - - index.addSymbol(IndexedSymbol( - name = "Exception", - fqName = "kotlin.Exception", - kind = IndexedSymbolKind.CLASS, - packageName = "kotlin", - superTypes = listOf("kotlin.Throwable") - )) - - addCollectionTypes(index) - } - - private fun addCollectionTypes(index: StdlibIndex) { - val collections = listOf( - Triple("Iterable", "INTERFACE", listOf("T")), - Triple("Collection", "INTERFACE", listOf("E")), - Triple("List", "INTERFACE", listOf("E")), - Triple("Set", "INTERFACE", listOf("E")), - Triple("Map", "INTERFACE", listOf("K", "V")), - Triple("MutableIterable", "INTERFACE", listOf("T")), - Triple("MutableCollection", "INTERFACE", listOf("E")), - Triple("MutableList", "INTERFACE", listOf("E")), - Triple("MutableSet", "INTERFACE", listOf("E")), - Triple("MutableMap", "INTERFACE", listOf("K", "V")), - Triple("Sequence", "INTERFACE", listOf("T")) - ) - - for ((name, kind, typeParams) in collections) { - index.addSymbol(IndexedSymbol( - name = name, - fqName = "kotlin.collections.$name", - kind = IndexedSymbolKind.valueOf(kind), - packageName = "kotlin.collections", - typeParameters = typeParams - )) - } - - index.addSymbol(IndexedSymbol( - name = "Pair", - fqName = "kotlin.Pair", - kind = IndexedSymbolKind.DATA_CLASS, - packageName = "kotlin", - typeParameters = listOf("A", "B") - )) - - index.addSymbol(IndexedSymbol( - name = "Triple", - fqName = "kotlin.Triple", - kind = IndexedSymbolKind.DATA_CLASS, - packageName = "kotlin", - typeParameters = listOf("A", "B", "C") - )) - } - - private fun addCoreFunctions(index: StdlibIndex) { - index.addSymbol(IndexedSymbol( - name = "println", - fqName = "kotlin.io.println", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin.io", - parameters = listOf(IndexedParameter("message", "Any?", hasDefault = true)), - returnType = "Unit" - )) - - index.addSymbol(IndexedSymbol( - name = "print", - fqName = "kotlin.io.print", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin.io", - parameters = listOf(IndexedParameter("message", "Any?")), - returnType = "Unit" - )) - - val collectionCreators = listOf( - "listOf" to "List", - "mutableListOf" to "MutableList", - "setOf" to "Set", - "mutableSetOf" to "MutableSet", - "arrayOf" to "Array", - "emptyList" to "List", - "emptySet" to "Set", - "emptyMap" to "Map" - ) - - for ((name, returnType) in collectionCreators) { - index.addSymbol(IndexedSymbol( - name = name, - fqName = "kotlin.collections.$name", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin.collections", - parameters = listOf(IndexedParameter("elements", "T", isVararg = true)), - returnType = returnType, - typeParameters = if (returnType.contains("Map")) listOf("K", "V") else listOf("T") - )) - } - - index.addSymbol(IndexedSymbol( - name = "mapOf", - fqName = "kotlin.collections.mapOf", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin.collections", - parameters = listOf(IndexedParameter("pairs", "Pair", isVararg = true)), - returnType = "Map", - typeParameters = listOf("K", "V") - )) - - index.addSymbol(IndexedSymbol( - name = "mutableMapOf", - fqName = "kotlin.collections.mutableMapOf", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin.collections", - parameters = listOf(IndexedParameter("pairs", "Pair", isVararg = true)), - returnType = "MutableMap", - typeParameters = listOf("K", "V") - )) - - addScopeFunctions(index) - } - - private fun addScopeFunctions(index: StdlibIndex) { - index.addSymbol(IndexedSymbol( - name = "let", - fqName = "kotlin.let", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin", - typeParameters = listOf("T", "R"), - parameters = listOf(IndexedParameter("block", "(T) -> R")), - returnType = "R", - receiverType = "T" - )) - - index.addSymbol(IndexedSymbol( - name = "run", - fqName = "kotlin.run", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin", - typeParameters = listOf("T", "R"), - parameters = listOf(IndexedParameter("block", "T.() -> R")), - returnType = "R", - receiverType = "T" - )) - - index.addSymbol(IndexedSymbol( - name = "with", - fqName = "kotlin.with", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin", - typeParameters = listOf("T", "R"), - parameters = listOf( - IndexedParameter("receiver", "T"), - IndexedParameter("block", "T.() -> R") - ), - returnType = "R" - )) - - index.addSymbol(IndexedSymbol( - name = "apply", - fqName = "kotlin.apply", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin", - typeParameters = listOf("T"), - parameters = listOf(IndexedParameter("block", "T.() -> Unit")), - returnType = "T", - receiverType = "T" - )) - - index.addSymbol(IndexedSymbol( - name = "also", - fqName = "kotlin.also", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin", - typeParameters = listOf("T"), - parameters = listOf(IndexedParameter("block", "(T) -> Unit")), - returnType = "T", - receiverType = "T" - )) - - index.addSymbol(IndexedSymbol( - name = "takeIf", - fqName = "kotlin.takeIf", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin", - typeParameters = listOf("T"), - parameters = listOf(IndexedParameter("predicate", "(T) -> Boolean")), - returnType = "T?", - receiverType = "T" - )) - - index.addSymbol(IndexedSymbol( - name = "takeUnless", - fqName = "kotlin.takeUnless", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin", - typeParameters = listOf("T"), - parameters = listOf(IndexedParameter("predicate", "(T) -> Boolean")), - returnType = "T?", - receiverType = "T" - )) - - index.addSymbol(IndexedSymbol( - name = "to", - fqName = "kotlin.to", - kind = IndexedSymbolKind.FUNCTION, - packageName = "kotlin", - typeParameters = listOf("A", "B"), - parameters = listOf(IndexedParameter("that", "B")), - returnType = "Pair", - receiverType = "A" - )) - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/SymbolIndex.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/SymbolIndex.kt deleted file mode 100644 index b644a36ec0..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/index/SymbolIndex.kt +++ /dev/null @@ -1,428 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.index - -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassKind -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Modifiers -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.PropertySymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolLocation -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeReference -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Visibility - -/** - * Interface for symbol lookup operations. - * - * SymbolIndex provides efficient lookup of symbols by various criteria: - * - By fully qualified name (exact match) - * - By simple name (may return multiple results) - * - By package (all symbols in a package) - * - By prefix (for code completion) - * - * Implementations include: - * - [FileIndex]: Per-file symbol storage - * - [ProjectIndex]: Project-wide aggregation - * - [StdlibIndex]: Kotlin standard library symbols - * - * ## Thread Safety - * - * Index implementations should be thread-safe for read operations. - * Write operations (add/remove) may require external synchronization. - * - * ## Usage - * - * ```kotlin - * val index: SymbolIndex = projectIndex - * val stringClass = index.findByFqName("kotlin.String") - * val listFunctions = index.findBySimpleName("listOf") - * val completions = index.findByPrefix("str") - * ``` - */ -interface SymbolIndex { - - /** - * Finds a symbol by its fully qualified name. - * - * @param fqName The fully qualified name (e.g., "kotlin.String") - * @return The indexed symbol, or null if not found - */ - fun findByFqName(fqName: String): IndexedSymbol? - - /** - * Finds all symbols with a given simple name. - * - * @param name The simple name (e.g., "String") - * @return List of matching symbols from all packages - */ - fun findBySimpleName(name: String): List - - /** - * Finds all symbols in a package. - * - * @param packageName The package name (e.g., "kotlin.collections") - * @return List of top-level symbols in that package - */ - fun findByPackage(packageName: String): List - - /** - * Finds symbols whose simple name starts with a prefix. - * - * Used for code completion. - * - * @param prefix The name prefix (e.g., "str" matches "String", "stringify") - * @param limit Maximum number of results (0 for unlimited) - * @return List of matching symbols - */ - fun findByPrefix(prefix: String, limit: Int = 0): List - - /** - * Gets all class symbols in the index. - */ - fun getAllClasses(): List - - /** - * Gets all function symbols in the index. - */ - fun getAllFunctions(): List - - /** - * Gets all property symbols in the index. - */ - fun getAllProperties(): List - - /** - * Gets the total number of indexed symbols. - */ - val size: Int - - /** - * Whether this index is empty. - */ - val isEmpty: Boolean get() = size == 0 - - /** - * Whether this index contains a symbol with the given FQ name. - */ - fun contains(fqName: String): Boolean = findByFqName(fqName) != null -} - -/** - * Mutable symbol index that supports adding and removing symbols. - */ -interface MutableSymbolIndex : SymbolIndex { - - /** - * Adds a symbol to the index. - * - * @param symbol The symbol to add - */ - fun add(symbol: IndexedSymbol) - - /** - * Adds multiple symbols to the index. - * - * @param symbols The symbols to add - */ - fun addAll(symbols: Iterable) { - symbols.forEach { add(it) } - } - - /** - * Removes a symbol from the index. - * - * @param fqName The fully qualified name of the symbol to remove - * @return true if the symbol was found and removed - */ - fun remove(fqName: String): Boolean - - /** - * Removes all symbols from the index. - */ - fun clear() -} - -/** - * Kind of indexed symbol. - */ -enum class IndexedSymbolKind { - CLASS, - INTERFACE, - OBJECT, - ENUM_CLASS, - ANNOTATION_CLASS, - DATA_CLASS, - VALUE_CLASS, - FUNCTION, - PROPERTY, - TYPE_ALIAS, - CONSTRUCTOR; - - val isClass: Boolean get() = this in setOf( - CLASS, INTERFACE, OBJECT, ENUM_CLASS, ANNOTATION_CLASS, DATA_CLASS, VALUE_CLASS - ) - - val isCallable: Boolean get() = this in setOf(FUNCTION, CONSTRUCTOR) - - companion object { - fun fromClassKind(kind: ClassKind): IndexedSymbolKind = when (kind) { - ClassKind.CLASS -> CLASS - ClassKind.INTERFACE -> INTERFACE - ClassKind.OBJECT -> OBJECT - ClassKind.COMPANION_OBJECT -> OBJECT - ClassKind.ENUM_CLASS -> ENUM_CLASS - ClassKind.ENUM_ENTRY -> OBJECT - ClassKind.ANNOTATION_CLASS -> ANNOTATION_CLASS - ClassKind.DATA_CLASS -> DATA_CLASS - ClassKind.VALUE_CLASS -> VALUE_CLASS - } - } -} - -/** - * A symbol stored in the index with metadata for efficient lookup. - * - * IndexedSymbol contains just enough information for: - * - Symbol identification (name, FQ name, kind) - * - Visibility filtering - * - Type signature (for overload resolution) - * - Documentation - * - * Full symbol information can be retrieved by loading the source file. - * - * @property name Simple name of the symbol - * @property fqName Fully qualified name - * @property kind The kind of symbol - * @property packageName The containing package - * @property containingClass FQ name of containing class (for members) - * @property visibility Symbol visibility - * @property signature Type signature for display - * @property typeParameters Generic type parameter names - * @property parameters Parameter information for functions - * @property returnType Return type for functions/properties - * @property receiverType Extension receiver type (if extension) - * @property superTypes Direct supertypes (for classes) - * @property filePath Source file path (if from project) - * @property deprecated Whether this symbol is deprecated - * @property deprecationMessage Deprecation message if deprecated - */ -data class IndexedSymbol( - val name: String, - val fqName: String, - val kind: IndexedSymbolKind, - val packageName: String, - val containingClass: String? = null, - val visibility: Visibility = Visibility.PUBLIC, - val signature: String = "", - val typeParameters: List = emptyList(), - val parameters: List = emptyList(), - val returnType: String? = null, - val receiverType: String? = null, - val superTypes: List = emptyList(), - val filePath: String? = null, - val startLine: Int? = null, - val startColumn: Int? = null, - val endLine: Int? = null, - val endColumn: Int? = null, - val deprecated: Boolean = false, - val deprecationMessage: String? = null -) { - /** - * Simple name for display. - */ - val simpleName: String get() = name - - /** - * Whether this is a top-level declaration. - */ - val isTopLevel: Boolean get() = containingClass == null - - /** - * Whether this is a member of a class. - */ - val isMember: Boolean get() = containingClass != null - - /** - * Whether this is an extension function/property. - */ - val isExtension: Boolean get() = receiverType != null - - /** - * Whether this is a generic symbol. - */ - val isGeneric: Boolean get() = typeParameters.isNotEmpty() - - /** - * Whether this symbol is from the standard library. - */ - val isStdlib: Boolean get() = packageName.startsWith("kotlin") - - /** - * Whether this symbol has source location information. - */ - val hasLocation: Boolean get() = startLine != null && startColumn != null - - /** - * Whether this symbol is visible from outside its module. - */ - val isPublicApi: Boolean get() = visibility == Visibility.PUBLIC || visibility == Visibility.PROTECTED - - /** - * The arity (parameter count) for functions. - */ - val arity: Int get() = parameters.size - - /** - * Creates a display string for the symbol. - */ - fun toDisplayString(): String = buildString { - when (kind) { - IndexedSymbolKind.CLASS -> append("class ") - IndexedSymbolKind.INTERFACE -> append("interface ") - IndexedSymbolKind.OBJECT -> append("object ") - IndexedSymbolKind.ENUM_CLASS -> append("enum class ") - IndexedSymbolKind.ANNOTATION_CLASS -> append("annotation class ") - IndexedSymbolKind.DATA_CLASS -> append("data class ") - IndexedSymbolKind.VALUE_CLASS -> append("value class ") - IndexedSymbolKind.FUNCTION -> append("fun ") - IndexedSymbolKind.PROPERTY -> append(if (returnType != null) "val " else "var ") - IndexedSymbolKind.TYPE_ALIAS -> append("typealias ") - IndexedSymbolKind.CONSTRUCTOR -> append("constructor") - } - - receiverType?.let { append(it).append('.') } - append(name) - - if (typeParameters.isNotEmpty()) { - append('<') - append(typeParameters.joinToString()) - append('>') - } - - if (kind.isCallable || kind == IndexedSymbolKind.CONSTRUCTOR) { - append('(') - append(parameters.joinToString { "${it.name}: ${it.type}" }) - append(')') - } - - returnType?.let { append(": ").append(it) } - } - - override fun toString(): String = fqName - - fun toSyntheticSymbol(): Symbol { - return when { - kind.isClass -> ClassSymbol( - name = fqName, - location = SymbolLocation.SYNTHETIC, - modifiers = Modifiers.EMPTY, - containingScope = null, - kind = when (kind) { - IndexedSymbolKind.CLASS -> ClassKind.CLASS - IndexedSymbolKind.INTERFACE -> ClassKind.INTERFACE - IndexedSymbolKind.OBJECT -> ClassKind.OBJECT - IndexedSymbolKind.ENUM_CLASS -> ClassKind.ENUM_CLASS - IndexedSymbolKind.ANNOTATION_CLASS -> ClassKind.ANNOTATION_CLASS - IndexedSymbolKind.DATA_CLASS -> ClassKind.DATA_CLASS - IndexedSymbolKind.VALUE_CLASS -> ClassKind.VALUE_CLASS - else -> ClassKind.CLASS - } - ) - kind == IndexedSymbolKind.FUNCTION || kind == IndexedSymbolKind.CONSTRUCTOR -> FunctionSymbol( - name = name, - location = SymbolLocation.SYNTHETIC, - modifiers = Modifiers.EMPTY, - containingScope = null, - typeParameters = typeParameters.map { tpName -> - TypeParameterSymbol( - name = tpName, - location = SymbolLocation.SYNTHETIC, - modifiers = Modifiers.EMPTY, - containingScope = null - ) - }, - parameters = parameters.map { - ParameterSymbol( - name = it.name, - location = SymbolLocation.SYNTHETIC, - modifiers = Modifiers.EMPTY, - containingScope = null, - type = TypeReference(it.type), - hasDefaultValue = it.hasDefault, - isVararg = it.isVararg - ) - }, - returnType = returnType?.let { TypeReference(it) }, - receiverType = receiverType?.let { TypeReference(it) } - ) - kind == IndexedSymbolKind.PROPERTY -> PropertySymbol( - name = name, - location = SymbolLocation.SYNTHETIC, - modifiers = Modifiers.EMPTY, - containingScope = null, - type = returnType?.let { TypeReference(it) }, - isVar = false, - receiverType = receiverType?.let { TypeReference(it) } - ) - else -> ClassSymbol( - name = fqName, - location = SymbolLocation.SYNTHETIC, - modifiers = Modifiers.EMPTY, - containingScope = null, - kind = ClassKind.CLASS - ) - } - } -} - -/** - * Parameter information for indexed functions. - */ -data class IndexedParameter( - val name: String, - val type: String, - val hasDefault: Boolean = false, - val isVararg: Boolean = false -) { - override fun toString(): String = buildString { - if (isVararg) append("vararg ") - append(name) - append(": ") - append(type) - if (hasDefault) append(" = ...") - } -} - -/** - * Index query options. - */ -data class IndexQuery( - val namePrefix: String? = null, - val packageName: String? = null, - val kinds: Set? = null, - val includeDeprecated: Boolean = true, - val includeInternal: Boolean = false, - val extensionReceiverType: String? = null, - val limit: Int = 0 -) { - companion object { - val ALL = IndexQuery() - - fun byName(name: String) = IndexQuery(namePrefix = name) - - fun byPackage(packageName: String) = IndexQuery(packageName = packageName) - - fun classes() = IndexQuery(kinds = setOf( - IndexedSymbolKind.CLASS, - IndexedSymbolKind.INTERFACE, - IndexedSymbolKind.OBJECT, - IndexedSymbolKind.ENUM_CLASS, - IndexedSymbolKind.DATA_CLASS - )) - - fun functions() = IndexQuery(kinds = setOf(IndexedSymbolKind.FUNCTION)) - - fun extensionsFor(receiverType: String) = IndexQuery(extensionReceiverType = receiverType) - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/KotlinParser.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/KotlinParser.kt deleted file mode 100644 index 6fd25e00e6..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/KotlinParser.kt +++ /dev/null @@ -1,221 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -import com.itsaky.androidide.treesitter.TSInputEdit -import com.itsaky.androidide.treesitter.TSParser -import com.itsaky.androidide.treesitter.TSPoint -import com.itsaky.androidide.treesitter.TSTree -import com.itsaky.androidide.treesitter.TreeSitter -import com.itsaky.androidide.treesitter.kotlin.TSLanguageKotlin - -/** - * Parses Kotlin source code into syntax trees using tree-sitter. - * - * This parser provides: - * - **Fast parsing**: Typical parse times are <10ms for most files - * - **Incremental parsing**: Re-parses only changed portions after edits - * - **Error tolerance**: Produces partial trees for syntactically invalid code - * - **Thread safety**: Each instance is NOT thread-safe; create one per thread - * - * ## Architecture - * - * This class wraps the AndroidIDE tree-sitter-kotlin parser. Tree-sitter is a fast, - * incremental parser generator that produces concrete syntax trees. The generated - * trees are: - * - Concrete (include all tokens, not just AST nodes) - * - Error-tolerant (partial trees for malformed input) - * - Incrementally updatable (efficient for editor use) - * - * ## Usage - * - * Basic parsing: - * ```kotlin - * val parser = KotlinParser() - * val result = parser.parse("fun main() { println(\"Hello\") }") - * - * result.tree.use { tree -> - * val root = tree.root - * println(root.kind) // SOURCE_FILE - * } - * ``` - * - * Incremental parsing: - * ```kotlin - * val parser = KotlinParser() - * var result = parser.parse(initialSource) - * - * // After user edits... - * val edit = SourceEdit(...) - * result = parser.parseIncremental(newSource, result.tree, edit) - * ``` - * - * ## Thread Safety - * - * Parser instances are NOT thread-safe. For concurrent parsing: - * - Create a parser instance per thread, or - * - Use synchronization when accessing shared parser instance - * - * ## Resource Management - * - * The parser uses native resources. Call [close] when done or use `use` blocks. - * - * @see SyntaxTree The parsed tree structure - * @see ParseResult Container for parse results with error information - */ -class KotlinParser : AutoCloseable { - - private val parser: TSParser - - init { - TreeSitter.loadLibrary() - parser = TSParser.create() - parser.language = TSLanguageKotlin.getInstance() - } - - /** - * The tree-sitter language version. - * Useful for debugging version mismatches. - */ - val languageVersion: Int - get() = 0 - - /** - * Parses Kotlin source code into a syntax tree. - * - * This method always succeeds, even for malformed input. Check - * [ParseResult.hasErrors] to see if syntax errors were found. - * - * ## Performance - * - * Parsing is typically fast (<10ms for small files, <100ms for large files). - * For repeated parsing of the same file with modifications, use - * [parseIncremental] instead. - * - * @param source The complete Kotlin source code to parse - * @param fileName Optional filename for error messages (not used for parsing) - * @return [ParseResult] containing the tree and any syntax errors - */ - fun parse(source: String, fileName: String = ""): ParseResult { - val startTime = System.currentTimeMillis() - - val tree = parser.parseString(source) - ?: throw IllegalStateException("Parser returned null tree for source") - - val syntaxTree = SyntaxTree(tree, source, languageVersion) - val errors = collectErrors(syntaxTree) - val parseTime = System.currentTimeMillis() - startTime - - return ParseResult( - tree = syntaxTree, - syntaxErrors = errors, - parseTimeMs = parseTime - ) - } - - /** - * Incrementally re-parses source code after an edit. - * - * Incremental parsing is much faster than full parsing for small edits. - * Tree-sitter reuses unchanged portions of the previous tree. - * - * ## When to Use - * - * Use incremental parsing when: - * - You have the previous tree from parsing the old source - * - You know what edit was made (position and length) - * - The edit is localized (not a complete rewrite) - * - * For large changes or when you don't have edit information, - * use regular [parse] instead. - * - * @param source The complete new source code (after the edit) - * @param previousTree The syntax tree from before the edit - * @param edit Description of what changed in the source - * @return [ParseResult] for the updated source - */ - fun parseIncremental( - source: String, - previousTree: SyntaxTree, - edit: SourceEdit - ): ParseResult { - val startTime = System.currentTimeMillis() - - val tsEdit = TSInputEdit.create( - edit.startByte, - edit.oldEndByte, - edit.newEndByte, - TSPoint.create(edit.startPosition.line, edit.startPosition.column), - TSPoint.create(edit.oldEndPosition.line, edit.oldEndPosition.column), - TSPoint.create(edit.newEndPosition.line, edit.newEndPosition.column) - ) - - previousTree.tree.edit(tsEdit) - - val newTree = parser.parseString(previousTree.tree, source) - ?: throw IllegalStateException("Incremental parse returned null tree") - - val syntaxTree = SyntaxTree(newTree, source, languageVersion) - val errors = collectErrors(syntaxTree) - val parseTime = System.currentTimeMillis() - startTime - - return ParseResult( - tree = syntaxTree, - syntaxErrors = errors, - parseTimeMs = parseTime - ) - } - - /** - * Sets the parsing timeout in microseconds. - * - * If parsing exceeds this timeout, it will be cancelled and return - * a partial tree. Set to 0 (default) for no timeout. - * - * Use this for responsive UI when parsing very large files. - * - * @param timeoutMicros Timeout in microseconds, or 0 for no timeout - */ - fun setTimeout(timeoutMicros: Long) { - require(timeoutMicros >= 0) { "Timeout must be non-negative" } - parser.timeout = timeoutMicros - } - - /** - * Gets the current parsing timeout in microseconds. - * - * @return Current timeout, or 0 if no timeout is set - */ - fun getTimeout(): Long = parser.timeout - - /** - * Resets the parser state. - * - * Call this if you want to ensure the parser starts fresh, - * discarding any internal state from previous parses. - */ - fun reset() { - parser.reset() - } - - /** - * Releases native resources held by this parser. - * - * After calling close(), the parser should not be used. - */ - override fun close() { - parser.close() - } - - /** - * Collects all syntax errors from the parsed tree. - * - * Walks the tree looking for ERROR and MISSING nodes, - * converting them to [SyntaxError] instances. - */ - private fun collectErrors(tree: SyntaxTree): List { - if (!tree.hasErrors) return emptyList() - - return tree.errors.map { errorNode -> - SyntaxError.fromNode(errorNode) - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/ParseResult.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/ParseResult.kt deleted file mode 100644 index 55cd57387d..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/ParseResult.kt +++ /dev/null @@ -1,315 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -/** - * Result of parsing Kotlin source code. - * - * Contains both the syntax tree and metadata about the parse operation. - * Even when parsing fails (syntax errors), a tree is still produced - * with error recovery. - * - * ## Error Handling - * - * Tree-sitter uses error recovery to produce a tree even for malformed input. - * The tree will contain ERROR nodes where the parser couldn't match the grammar. - * This allows the LSP to provide functionality even while code is being edited. - * - * ## Usage - * - * ```kotlin - * val result = parser.parse(source) - * - * if (result.hasErrors) { - * for (error in result.syntaxErrors) { - * println("Syntax error at ${error.range}: ${error.message}") - * } - * } - * - * // Tree is usable regardless of errors - * val functions = result.tree.root.findAllByKind(SyntaxKind.FUNCTION_DECLARATION) - * ``` - * - * ## Resource Management - * - * The [tree] holds native resources. Use `use` blocks for automatic cleanup: - * - * ```kotlin - * parser.parse(source).use { result -> - * // Use result.tree... - * } // Tree is closed automatically - * ``` - * - * @property tree The parsed syntax tree - * @property syntaxErrors List of syntax errors found during parsing - * @property parseTimeMs Time taken to parse, in milliseconds - */ -data class ParseResult( - val tree: SyntaxTree, - val syntaxErrors: List, - val parseTimeMs: Long -) : AutoCloseable { - - /** - * Whether any syntax errors were found. - */ - val hasErrors: Boolean get() = syntaxErrors.isNotEmpty() - - /** - * The number of syntax errors found. - */ - val errorCount: Int get() = syntaxErrors.size - - /** - * Whether the parse succeeded without any errors. - */ - val isValid: Boolean get() = !hasErrors - - /** - * Closes the underlying syntax tree and releases resources. - */ - override fun close() { - tree.close() - } -} - -/** - * Represents a syntax error found during parsing. - * - * Syntax errors occur when the source doesn't match the Kotlin grammar. - * Tree-sitter recovers from errors and continues parsing, so multiple - * errors may be reported for a single parse. - * - * @property range Where the error occurred in the source - * @property message Human-readable description of the error - * @property errorNodeText The problematic text that caused the error - */ -data class SyntaxError( - val range: TextRange, - val message: String, - val errorNodeText: String -) { - /** - * One-based line number for display. - */ - val line: Int get() = range.start.displayLine - - /** - * One-based column number for display. - */ - val column: Int get() = range.start.displayColumn - - override fun toString(): String { - return "SyntaxError(${range.start}: $message)" - } - - companion object { - /** - * Creates a SyntaxError from an error node in the tree. - * - * @param node The ERROR or MISSING node from tree-sitter - * @return A SyntaxError with details about the parse failure - */ - fun fromNode(node: SyntaxNode): SyntaxError { - val message = when { - node.isMissing -> "Missing ${formatNodeType(node.type)}" - node.kind == SyntaxKind.ERROR -> "Unexpected syntax" - else -> "Syntax error" - } - - val errorRange = narrowErrorRange(node) - - return SyntaxError( - range = errorRange, - message = message, - errorNodeText = node.text.take(50) - ) - } - - private fun narrowErrorRange(node: SyntaxNode): TextRange { - if (node.isMissing) { - return node.range - } - - val missingNode = findFirstMissingNode(node) - if (missingNode != null) { - return missingNode.range - } - - val firstErrorChild = findFirstErrorChild(node) - if (firstErrorChild != null && firstErrorChild !== node) { - return narrowErrorRange(firstErrorChild) - } - - val start = node.range.start - val end = if (node.range.end.line == start.line) { - Position(start.line, minOf(start.column + 20, node.range.end.column)) - } else { - Position(start.line, start.column + 1) - } - - return TextRange(start, end) - } - - private fun findFirstMissingNode(node: SyntaxNode): SyntaxNode? { - if (node.isMissing) { - return node - } - for (child in node.children) { - val found = findFirstMissingNode(child) - if (found != null) { - return found - } - } - return null - } - - private fun findFirstErrorChild(node: SyntaxNode): SyntaxNode? { - for (child in node.children) { - if (child.kind == SyntaxKind.ERROR) { - return child - } - val nested = findFirstErrorChild(child) - if (nested != null) { - return nested - } - } - return null - } - - /** - * Formats a tree-sitter node type for display. - * - * Converts snake_case to readable text: "function_declaration" → "function declaration" - */ - private fun formatNodeType(type: String): String { - return type.replace('_', ' ') - } - } -} - -/** - * Represents an edit to be applied before incremental parsing. - * - * When source code is modified, describe the edit using this class - * and pass it to [KotlinParser.parseIncremental] for efficient re-parsing. - * - * ## Byte Offsets - * - * All offsets are in bytes, not characters. For ASCII text these are the same, - * but for UTF-8 text with multi-byte characters, byte counts may differ. - * - * ## Example - * - * Inserting "hello" at byte position 10: - * ```kotlin - * val edit = SourceEdit( - * startByte = 10, - * oldEndByte = 10, // No text replaced (insertion) - * newEndByte = 15, // 5 bytes inserted - * startPosition = Position(0, 10), - * oldEndPosition = Position(0, 10), - * newEndPosition = Position(0, 15) - * ) - * ``` - * - * Replacing "foo" (3 bytes) with "bar" (3 bytes) at position 10: - * ```kotlin - * val edit = SourceEdit( - * startByte = 10, - * oldEndByte = 13, // Old text was 3 bytes - * newEndByte = 13, // New text is also 3 bytes - * startPosition = Position(0, 10), - * oldEndPosition = Position(0, 13), - * newEndPosition = Position(0, 13) - * ) - * ``` - * - * @property startByte Byte offset where the edit starts - * @property oldEndByte Byte offset where the OLD text ended (before edit) - * @property newEndByte Byte offset where the NEW text ends (after edit) - * @property startPosition Position where the edit starts - * @property oldEndPosition Position where the old text ended - * @property newEndPosition Position where the new text ends - */ -data class SourceEdit( - val startByte: Int, - val oldEndByte: Int, - val newEndByte: Int, - val startPosition: Position, - val oldEndPosition: Position, - val newEndPosition: Position -) { - init { - require(startByte >= 0) { "startByte must be non-negative" } - require(oldEndByte >= startByte) { "oldEndByte must be >= startByte" } - require(newEndByte >= startByte) { "newEndByte must be >= startByte" } - } - - /** - * Number of bytes in the old (replaced) text. - */ - val oldLength: Int get() = oldEndByte - startByte - - /** - * Number of bytes in the new (replacement) text. - */ - val newLength: Int get() = newEndByte - startByte - - /** - * Whether this edit is an insertion (no old text replaced). - */ - val isInsertion: Boolean get() = oldLength == 0 - - /** - * Whether this edit is a deletion (no new text inserted). - */ - val isDeletion: Boolean get() = newLength == 0 - - /** - * Whether this edit replaces text (both old and new lengths > 0). - */ - val isReplacement: Boolean get() = oldLength > 0 && newLength > 0 - - companion object { - /** - * Creates an edit representing an insertion. - * - * @param position Where to insert - * @param byteOffset Byte offset of insertion point - * @param insertedLength Length of inserted text in bytes - */ - fun insertion(position: Position, byteOffset: Int, insertedLength: Int): SourceEdit { - return SourceEdit( - startByte = byteOffset, - oldEndByte = byteOffset, - newEndByte = byteOffset + insertedLength, - startPosition = position, - oldEndPosition = position, - newEndPosition = position.copy(column = position.column + insertedLength) - ) - } - - /** - * Creates an edit representing a deletion. - * - * @param startPosition Start of deleted range - * @param endPosition End of deleted range - * @param startByte Byte offset of deletion start - * @param endByte Byte offset of deletion end - */ - fun deletion( - startPosition: Position, - endPosition: Position, - startByte: Int, - endByte: Int - ): SourceEdit { - return SourceEdit( - startByte = startByte, - oldEndByte = endByte, - newEndByte = startByte, - startPosition = startPosition, - oldEndPosition = endPosition, - newEndPosition = startPosition - ) - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/Position.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/Position.kt deleted file mode 100644 index b9e1a4aa0a..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/Position.kt +++ /dev/null @@ -1,105 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -/** - * Represents a position within source code using line and column numbers. - * - * ## Coordinate System - * - * This class uses **zero-based** line and column numbers internally (matching tree-sitter), - * but provides conversion methods for **one-based** coordinates used in editors and error messages. - * - * ``` - * Line 0: fun main() { - * ^ ^ - * | column 4 - * column 0 - * - * Line 1: println("Hello") - * ``` - * - * ## Immutability - * - * Position instances are immutable. Operations that modify position return new instances. - * - * ## Usage - * - * ```kotlin - * val pos = Position(line = 0, column = 5) - * val display = "Error at line ${pos.displayLine}, column ${pos.displayColumn}" - * ``` - * - * @property line Zero-based line number - * @property column Zero-based column number (byte offset within line) - */ -data class Position( - val line: Int, - val column: Int -) : Comparable { - - init { - require(line >= 0) { "Line number must be non-negative, got $line" } - require(column >= 0) { "Column number must be non-negative, got $column" } - } - - /** - * One-based line number for display in editors and error messages. - * Line 0 becomes line 1, etc. - */ - val displayLine: Int get() = line + 1 - - /** - * One-based column number for display in editors and error messages. - * Column 0 becomes column 1, etc. - */ - val displayColumn: Int get() = column + 1 - - /** - * Compares positions lexicographically (line first, then column). - * - * @param other The position to compare with - * @return Negative if this comes before other, positive if after, zero if equal - */ - override fun compareTo(other: Position): Int { - val lineCompare = line.compareTo(other.line) - return if (lineCompare != 0) lineCompare else column.compareTo(other.column) - } - - /** - * Creates a new position offset by the given line and column deltas. - * - * @param lineDelta Number of lines to add (can be negative) - * @param columnDelta Number of columns to add (can be negative) - * @return New position with applied offsets - * @throws IllegalArgumentException if resulting position would have negative coordinates - */ - fun offset(lineDelta: Int, columnDelta: Int): Position { - return Position( - line = line + lineDelta, - column = column + columnDelta - ) - } - - /** - * Returns a human-readable string for display purposes. - * Uses one-based line and column numbers. - */ - override fun toString(): String = "$displayLine:$displayColumn" - - companion object { - /** Position at the start of a document */ - val ZERO: Position = Position(0, 0) - - /** - * Creates a position from one-based (display) coordinates. - * - * @param displayLine One-based line number (as shown in editors) - * @param displayColumn One-based column number - * @return Position with zero-based coordinates - */ - fun fromDisplay(displayLine: Int, displayColumn: Int): Position { - require(displayLine >= 1) { "Display line must be >= 1, got $displayLine" } - require(displayColumn >= 1) { "Display column must be >= 1, got $displayColumn" } - return Position(displayLine - 1, displayColumn - 1) - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/PositionConverter.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/PositionConverter.kt deleted file mode 100644 index f10d546143..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/PositionConverter.kt +++ /dev/null @@ -1,109 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -object PositionConverter { - - fun byteOffsetToCharIndex(source: String, byteOffset: Int): Int { - if (byteOffset <= 0) return 0 - if (source.isEmpty()) return 0 - - var byteCount = 0 - var charIndex = 0 - - while (charIndex < source.length && byteCount < byteOffset) { - val char = source[charIndex] - - byteCount += when { - Character.isHighSurrogate(char) -> 4 - Character.isLowSurrogate(char) -> 0 - else -> 2 - } - charIndex++ - } - - return minOf(charIndex, source.length) - } - - fun charIndexToByteOffset(source: String, charIndex: Int): Int { - if (charIndex <= 0) return 0 - if (source.isEmpty()) return 0 - - var byteCount = 0 - val endIndex = minOf(charIndex, source.length) - - for (i in 0 until endIndex) { - val char = source[i] - - byteCount += when { - Character.isHighSurrogate(char) -> 4 - Character.isLowSurrogate(char) -> 0 - else -> 2 - } - } - - return byteCount - } - - fun columnToCharColumn(lineText: String, column: Int): Int { - return byteOffsetToCharIndex(lineText, column) - } - - fun charColumnToColumn(lineText: String, charColumn: Int): Int { - return charIndexToByteOffset(lineText, charColumn) - } - - fun lineAndColumnToCharOffset(source: String, line: Int, column: Int): Int { - if (source.isEmpty()) return 0 - - var currentLine = 0 - var lineStart = 0 - - for (i in source.indices) { - if (currentLine == line) { - break - } - if (source[i] == '\n') { - currentLine++ - lineStart = i + 1 - } - } - - if (currentLine != line) { - return source.length - } - - val lineEnd = source.indexOf('\n', lineStart).let { if (it < 0) source.length else it } - val lineText = source.substring(lineStart, lineEnd) - val charColumn = columnToCharColumn(lineText, column) - - return minOf(lineStart + charColumn, source.length) - } - - fun charOffsetToLineAndColumn(source: String, charOffset: Int): Pair { - if (source.isEmpty()) return 0 to 0 - - var line = 0 - var lineStart = 0 - val safeOffset = minOf(charOffset, source.length) - - for (i in 0 until safeOffset) { - if (source[i] == '\n') { - line++ - lineStart = i + 1 - } - } - - val column = safeOffset - lineStart - return line to column - } - - fun extractSubstring(source: String, startByte: Int, endByte: Int): String { - val startChar = byteOffsetToCharIndex(source, startByte) - val endChar = byteOffsetToCharIndex(source, endByte) - - return if (startChar >= 0 && endChar <= source.length && startChar <= endChar) { - source.substring(startChar, endChar) - } else { - "" - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxKind.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxKind.kt deleted file mode 100644 index 4cc987b28e..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxKind.kt +++ /dev/null @@ -1,1048 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -/** - * Represents all possible syntax node types in Kotlin source code. - * - * These values correspond to tree-sitter-kotlin grammar node types. The naming follows - * Kotlin enum conventions (SCREAMING_SNAKE_CASE) while storing the original tree-sitter - * names (snake_case) for lookup. - * - * ## Usage - * - * ```kotlin - * val kind = SyntaxKind.fromTreeSitter("function_declaration") - * when (kind) { - * SyntaxKind.FUNCTION_DECLARATION -> handleFunction(node) - * SyntaxKind.CLASS_DECLARATION -> handleClass(node) - * else -> {} - * } - * ``` - * - * ## Design Note - * - * This enum serves as an abstraction layer between tree-sitter's string-based node types - * and our type-safe Kotlin code. By centralizing all node type mappings here, we: - * 1. Get compile-time checking for node type handling - * 2. Can use exhaustive `when` expressions - * 3. Isolate tree-sitter grammar changes to a single location - * - * @property treeSitterName The exact string used by tree-sitter-kotlin grammar - * @see tree-sitter-kotlin grammar - */ -enum class SyntaxKind(val treeSitterName: String) { - - // ═══════════════════════════════════════════════════════════════════════════════ - // Source Structure - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Root node of any Kotlin source file */ - SOURCE_FILE("source_file"), - - /** Package declaration: `package com.example` */ - PACKAGE_HEADER("package_header"), - - /** Container for all import statements */ - IMPORT_LIST("import_list"), - - /** Single import statement: `import kotlin.collections.List` */ - IMPORT_HEADER("import_header"), - - /** Alias in import: `import foo.Bar as Baz` */ - IMPORT_ALIAS("import_alias"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Declarations - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Class declaration: `class Foo { }` */ - CLASS_DECLARATION("class_declaration"), - - /** Object declaration: `object Singleton { }` */ - OBJECT_DECLARATION("object_declaration"), - - /** Companion object: `companion object { }` */ - COMPANION_OBJECT("companion_object"), - - /** Interface declaration: `interface Drawable { }` */ - INTERFACE_DECLARATION("interface_declaration"), - - /** Function declaration: `fun foo() { }` */ - FUNCTION_DECLARATION("function_declaration"), - - /** Function body: `{ statements }` or `= expression` */ - FUNCTION_BODY("function_body"), - - /** Property declaration: `val x = 1` or `var y: Int` */ - PROPERTY_DECLARATION("property_declaration"), - - /** Multi-variable declaration (destructuring): `val (x, y) = pair` */ - MULTI_VARIABLE_DECLARATION("multi_variable_declaration"), - - /** Variable declaration (in destructuring): `x` in `val (x, y)` */ - VARIABLE_DECLARATION("variable_declaration"), - - /** Getter accessor: `get() = field` */ - GETTER("getter"), - - /** Setter accessor: `set(value) { field = value }` */ - SETTER("setter"), - - /** Type alias: `typealias StringList = List` */ - TYPE_ALIAS("type_alias"), - - /** Enum class declaration */ - ENUM_CLASS_BODY("enum_class_body"), - - /** Single enum entry */ - ENUM_ENTRY("enum_entry"), - - /** Anonymous initializer block: `init { }` */ - ANONYMOUS_INITIALIZER("anonymous_initializer"), - - /** Secondary constructor: `constructor(x: Int) : this()` */ - SECONDARY_CONSTRUCTOR("secondary_constructor"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Class Body & Members - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Body of a class/interface/object */ - CLASS_BODY("class_body"), - - /** Primary constructor parameters */ - PRIMARY_CONSTRUCTOR("primary_constructor"), - - /** Delegation specifiers (superclass, interfaces) */ - DELEGATION_SPECIFIER("delegation_specifier"), - - /** Delegation specifiers list */ - DELEGATION_SPECIFIERS("delegation_specifiers"), - - /** Constructor delegation call */ - CONSTRUCTOR_DELEGATION_CALL("constructor_delegation_call"), - - /** Constructor invocation: `Foo()` in inheritance */ - CONSTRUCTOR_INVOCATION("constructor_invocation"), - - /** Explicit delegation: `by delegate` */ - EXPLICIT_DELEGATION("explicit_delegation"), - - /** Annotated delegation specifier */ - ANNOTATED_DELEGATION_SPECIFIER("annotated_delegation_specifier"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Parameters & Arguments - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Function parameter list: `(a: Int, b: String)` */ - FUNCTION_VALUE_PARAMETERS("function_value_parameters"), - - /** Single function parameter */ - PARAMETER("parameter"), - - /** Parameter with modifiers */ - PARAMETER_WITH_OPTIONAL_TYPE("parameter_with_optional_type"), - - /** Class parameter (in primary constructor) */ - CLASS_PARAMETER("class_parameter"), - - /** Receiver parameter for extension functions */ - FUNCTION_TYPE_PARAMETERS("function_type_parameters"), - - /** Value arguments in function call: `foo(1, "hello")` */ - VALUE_ARGUMENTS("value_arguments"), - - /** Single value argument */ - VALUE_ARGUMENT("value_argument"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Type Parameters & Arguments (Generics) - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Type parameters: `` */ - TYPE_PARAMETERS("type_parameters"), - - /** Single type parameter: `T` or `T : Comparable` */ - TYPE_PARAMETER("type_parameter"), - - /** Type arguments: `` */ - TYPE_ARGUMENTS("type_arguments"), - - /** Type projection: `out T`, `in T`, `*` */ - TYPE_PROJECTION("type_projection"), - - /** Type constraints: `where T : Comparable, T : Serializable` */ - TYPE_CONSTRAINTS("type_constraints"), - - /** Single type constraint */ - TYPE_CONSTRAINT("type_constraint"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Types - // ═══════════════════════════════════════════════════════════════════════════════ - - /** User-defined type reference: `List` */ - USER_TYPE("user_type"), - - /** Simple user type (single identifier with optional type args) */ - SIMPLE_USER_TYPE("simple_user_type"), - - /** Nullable type: `String?` */ - NULLABLE_TYPE("nullable_type"), - - /** Parenthesized type: `(Int -> String)` */ - PARENTHESIZED_TYPE("parenthesized_type"), - - /** Function type: `(Int, String) -> Boolean` */ - FUNCTION_TYPE("function_type"), - - /** Function type parameters (in function type) */ - FUNCTION_TYPE_PARAMETER("function_type_parameter"), - - /** Dynamic type (for JavaScript interop) */ - DYNAMIC_TYPE("dynamic_type"), - - /** Type modifier (suspend, etc.) */ - TYPE_MODIFIER("type_modifier"), - - /** Receiver type in extension functions */ - RECEIVER_TYPE("receiver_type"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Expressions - Primary - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Parenthesized expression: `(x + y)` */ - PARENTHESIZED_EXPRESSION("parenthesized_expression"), - - /** Collection literal: `[1, 2, 3]` (annotations only) */ - COLLECTION_LITERAL("collection_literal"), - - /** This expression: `this` or `this@Outer` */ - THIS_EXPRESSION("this_expression"), - - /** Super expression: `super` or `super@Outer` */ - SUPER_EXPRESSION("super_expression"), - - /** If expression: `if (cond) a else b` */ - IF_EXPRESSION("if_expression"), - - /** When expression (Kotlin's switch): `when (x) { ... }` */ - WHEN_EXPRESSION("when_expression"), - - /** When subject: `when (val x = expr)` */ - WHEN_SUBJECT("when_subject"), - - /** Single when entry: `is String -> ...` */ - WHEN_ENTRY("when_entry"), - - /** When condition */ - WHEN_CONDITION("when_condition"), - - /** Range test in when: `in 1..10` */ - RANGE_TEST("range_test"), - - /** Type test in when: `is String` */ - TYPE_TEST("type_test"), - - /** Try expression with catch/finally */ - TRY_EXPRESSION("try_expression"), - - /** Catch block */ - CATCH_BLOCK("catch_block"), - - /** Finally block */ - FINALLY_BLOCK("finally_block"), - - /** Jump expression: return, break, continue, throw */ - JUMP_EXPRESSION("jump_expression"), - - /** Object literal: `object : Interface { }` */ - OBJECT_LITERAL("object_literal"), - - /** Lambda literal: `{ x -> x + 1 }` */ - LAMBDA_LITERAL("lambda_literal"), - - /** Lambda parameters */ - LAMBDA_PARAMETERS("lambda_parameters"), - - /** Annotated lambda */ - ANNOTATED_LAMBDA("annotated_lambda"), - - /** Anonymous function: `fun(x: Int): Int = x + 1` */ - ANONYMOUS_FUNCTION("anonymous_function"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Expressions - Operations - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Function/method call: `foo(args)` */ - CALL_EXPRESSION("call_expression"), - - /** Indexing: `array[index]` */ - INDEXING_EXPRESSION("indexing_expression"), - - /** Navigation/member access: `obj.member` or `obj?.member` */ - NAVIGATION_EXPRESSION("navigation_expression"), - - /** Navigation suffix (the part after the dot) */ - NAVIGATION_SUFFIX("navigation_suffix"), - - /** Call suffix (parentheses and arguments) */ - CALL_SUFFIX("call_suffix"), - - /** Indexing suffix (brackets and indices) */ - INDEXING_SUFFIX("indexing_suffix"), - - /** Prefix unary expression: `!x`, `-y`, `++z` */ - PREFIX_EXPRESSION("prefix_expression"), - - /** Postfix unary expression: `x++`, `y--`, `z!!` */ - POSTFIX_EXPRESSION("postfix_expression"), - - /** As expression (type cast): `x as String` */ - AS_EXPRESSION("as_expression"), - - /** Spread operator: `*array` */ - SPREAD_EXPRESSION("spread_expression"), - - /** Binary expression: `a + b`, `x == y` */ - BINARY_EXPRESSION("binary_expression"), - - /** Infix function call: `a to b` */ - INFIX_EXPRESSION("infix_expression"), - - /** Elvis expression: `x ?: default` */ - ELVIS_EXPRESSION("elvis_expression"), - - /** Check expression (in, !in, is, !is) */ - CHECK_EXPRESSION("check_expression"), - - /** Comparison expression: `a < b` */ - COMPARISON_EXPRESSION("comparison_expression"), - - /** Equality expression: `a == b` */ - EQUALITY_EXPRESSION("equality_expression"), - - /** Conjunction (and): `a && b` */ - CONJUNCTION_EXPRESSION("conjunction_expression"), - - /** Disjunction (or): `a || b` */ - DISJUNCTION_EXPRESSION("disjunction_expression"), - - /** Additive expression: `a + b` */ - ADDITIVE_EXPRESSION("additive_expression"), - - /** Multiplicative expression: `a * b` */ - MULTIPLICATIVE_EXPRESSION("multiplicative_expression"), - - /** Range expression: `1..10` or `1..<10` */ - RANGE_EXPRESSION("range_expression"), - - /** Assignment: `x = y` */ - ASSIGNMENT("assignment"), - - /** Augmented assignment: `x += 1` */ - AUGMENTED_ASSIGNMENT("augmented_assignment"), - - /** Directly assignable expression: wraps the left side of an assignment */ - DIRECTLY_ASSIGNABLE_EXPRESSION("directly_assignable_expression"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Expressions - String Templates - // ═══════════════════════════════════════════════════════════════════════════════ - - /** String literal with possible interpolation */ - STRING_LITERAL("string_literal"), - - /** Line string literal: `"hello"` */ - LINE_STRING_LITERAL("line_string_literal"), - - /** Multi-line string literal: `"""..."""` */ - MULTI_LINE_STRING_LITERAL("multi_line_string_literal"), - - /** String content (text between interpolations) */ - STRING_CONTENT("string_content"), - - /** Line string content */ - LINE_STRING_CONTENT("line_string_content"), - - /** Multi-line string content */ - MULTI_LINE_STRING_CONTENT("multi_line_string_content"), - - /** String interpolation entry: `$name` or `${expr}` */ - INTERPOLATION("interpolation"), - - /** Line string expression */ - LINE_STRING_EXPRESSION("line_string_expression"), - - /** Multi-line string expression */ - MULTI_LINE_STRING_EXPRESSION("multi_line_string_expression"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Literals - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Integer literal: `42`, `0xFF`, `0b1010` */ - INTEGER_LITERAL("integer_literal"), - - /** Long literal: `42L` */ - LONG_LITERAL("long_literal"), - - /** Hex literal: `0xFF` */ - HEX_LITERAL("hex_literal"), - - /** Binary literal: `0b1010` */ - BIN_LITERAL("bin_literal"), - - /** Real/floating-point literal: `3.14`, `1e10` */ - REAL_LITERAL("real_literal"), - - /** Boolean literal: `true` or `false` */ - BOOLEAN_LITERAL("boolean_literal"), - - /** Character literal: `'a'` */ - CHARACTER_LITERAL("character_literal"), - - /** Null literal: `null` */ - NULL_LITERAL("null_literal"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Identifiers & References - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Simple identifier: `foo`, `bar` */ - SIMPLE_IDENTIFIER("simple_identifier"), - - /** Type identifier (used for type names) */ - TYPE_IDENTIFIER("type_identifier"), - - /** Identifier (general) */ - IDENTIFIER("identifier"), - - /** Label: `loop@` */ - LABEL("label"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Statements - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Block of statements: `{ ... }` */ - STATEMENTS("statements"), - - /** For loop: `for (x in list) { }` */ - FOR_STATEMENT("for_statement"), - - /** While loop: `while (cond) { }` */ - WHILE_STATEMENT("while_statement"), - - /** Do-while loop: `do { } while (cond)` */ - DO_WHILE_STATEMENT("do_while_statement"), - - /** Control structure body (block or single statement) */ - CONTROL_STRUCTURE_BODY("control_structure_body"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Modifiers - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Container for modifiers */ - MODIFIERS("modifiers"), - - /** Single modifier */ - MODIFIER("modifier"), - - /** Class modifier: `enum`, `sealed`, `annotation`, `data`, `inner`, `value` */ - CLASS_MODIFIER("class_modifier"), - - /** Member modifier: `override`, `lateinit` */ - MEMBER_MODIFIER("member_modifier"), - - /** Visibility modifier: `public`, `private`, `protected`, `internal` */ - VISIBILITY_MODIFIER("visibility_modifier"), - - /** Variance modifier: `in`, `out` */ - VARIANCE_MODIFIER("variance_modifier"), - - /** Type parameter modifier: `reified` */ - TYPE_PARAMETER_MODIFIER("type_parameter_modifier"), - - /** Function modifier: `tailrec`, `operator`, `infix`, `inline`, `external`, `suspend` */ - FUNCTION_MODIFIER("function_modifier"), - - /** Property modifier: `const` */ - PROPERTY_MODIFIER("property_modifier"), - - /** Inheritance modifier: `abstract`, `final`, `open` */ - INHERITANCE_MODIFIER("inheritance_modifier"), - - /** Parameter modifier: `vararg`, `noinline`, `crossinline` */ - PARAMETER_MODIFIER("parameter_modifier"), - - /** Platform modifier: `expect`, `actual` */ - PLATFORM_MODIFIER("platform_modifier"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Annotations - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Annotation: `@Deprecated` */ - ANNOTATION("annotation"), - - /** Single annotation entry */ - SINGLE_ANNOTATION("single_annotation"), - - /** Multiple annotations: `@A @B` */ - MULTI_ANNOTATION("multi_annotation"), - - /** Annotation use-site target: `@field:Inject` */ - ANNOTATION_USE_SITE_TARGET("annotation_use_site_target"), - - /** Unescaped annotation */ - UNESCAPED_ANNOTATION("unescaped_annotation"), - - /** File annotation: `@file:JvmName("Utils")` */ - FILE_ANNOTATION("file_annotation"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Operators - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Prefix operator: `!`, `-`, `+`, `++`, `--` */ - PREFIX_UNARY_OPERATOR("prefix_unary_operator"), - - /** Postfix operator: `++`, `--`, `!!` */ - POSTFIX_UNARY_OPERATOR("postfix_unary_operator"), - - /** Multiplicative operator: `*`, `/`, `%` */ - MULTIPLICATIVE_OPERATOR("multiplicative_operator"), - - /** Additive operator: `+`, `-` */ - ADDITIVE_OPERATOR("additive_operator"), - - /** Comparison operator: `<`, `>`, `<=`, `>=` */ - COMPARISON_OPERATOR("comparison_operator"), - - /** Equality operator: `==`, `!=`, `===`, `!==` */ - EQUALITY_OPERATOR("equality_operator"), - - /** Assignment operator: `=` */ - ASSIGNMENT_OPERATOR("assignment_operator"), - - /** Augmented assignment operator: `+=`, `-=`, etc. */ - AUGMENTED_ASSIGNMENT_OPERATOR("augmented_assignment_operator"), - - /** Member access operator: `.`, `?.`, `::` */ - MEMBER_ACCESS_OPERATOR("member_access_operator"), - - /** In operator: `in`, `!in` */ - IN_OPERATOR("in_operator"), - - /** Is operator: `is`, `!is` */ - IS_OPERATOR("is_operator"), - - /** As operator: `as`, `as?` */ - AS_OPERATOR("as_operator"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Punctuation & Keywords - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Semicolon */ - SEMI("semi"), - - /** Newline (semis) */ - SEMIS("semis"), - - /** Colon */ - COLON("colon"), - - /** Comma */ - COMMA("comma"), - - /** Left parenthesis */ - LPAREN("("), - - /** Right parenthesis */ - RPAREN(")"), - - /** Left brace */ - LBRACE("{"), - - /** Right brace */ - RBRACE("}"), - - /** Left bracket */ - LBRACKET("["), - - /** Right bracket */ - RBRACKET("]"), - - /** Left angle bracket */ - LANGLE("<"), - - /** Right angle bracket */ - RANGLE(">"), - - /** Dot */ - DOT("."), - - /** Safe call operator */ - SAFE_NAV("?."), - - /** Double colon (callable reference) */ - COLONCOLON("::"), - - /** Range operator */ - RANGE(".."), - - /** Range until operator */ - RANGE_UNTIL("..<"), - - /** Spread operator */ - SPREAD("*"), - - /** Elvis operator */ - ELVIS("?:"), - - /** Arrow */ - ARROW("->"), - - /** Double arrow */ - DOUBLE_ARROW("=>"), - - /** At sign */ - AT("@"), - - /** Question mark */ - QUEST("?"), - - /** Exclamation mark */ - EXCL("!"), - - /** Double exclamation (non-null assertion) */ - EXCL_EXCL("!!"), - - /** Underscore (unused parameter) */ - UNDERSCORE("_"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Keywords - // ═══════════════════════════════════════════════════════════════════════════════ - - /** `package` keyword */ - PACKAGE("package"), - - /** `import` keyword */ - IMPORT("import"), - - /** `class` keyword */ - CLASS("class"), - - /** `interface` keyword */ - INTERFACE("interface"), - - /** `fun` keyword */ - FUN("fun"), - - /** `object` keyword */ - OBJECT("object"), - - /** `val` keyword */ - VAL("val"), - - /** `var` keyword */ - VAR("var"), - - /** `typealias` keyword */ - TYPEALIAS("typealias"), - - /** `constructor` keyword */ - CONSTRUCTOR("constructor"), - - /** `by` keyword (delegation) */ - BY("by"), - - /** `companion` keyword */ - COMPANION("companion"), - - /** `init` keyword */ - INIT("init"), - - /** `this` keyword */ - THIS("this"), - - /** `super` keyword */ - SUPER("super"), - - /** `typeof` keyword (reserved) */ - TYPEOF("typeof"), - - /** `where` keyword */ - WHERE("where"), - - /** `if` keyword */ - IF("if"), - - /** `else` keyword */ - ELSE("else"), - - /** `when` keyword */ - WHEN("when"), - - /** `try` keyword */ - TRY("try"), - - /** `catch` keyword */ - CATCH("catch"), - - /** `finally` keyword */ - FINALLY("finally"), - - /** `for` keyword */ - FOR("for"), - - /** `do` keyword */ - DO("do"), - - /** `while` keyword */ - WHILE("while"), - - /** `throw` keyword */ - THROW("throw"), - - /** `return` keyword */ - RETURN("return"), - - /** `continue` keyword */ - CONTINUE("continue"), - - /** `break` keyword */ - BREAK("break"), - - /** `as` keyword */ - AS("as"), - - /** `is` keyword */ - IS("is"), - - /** `in` keyword */ - IN("in"), - - /** `out` keyword */ - OUT("out"), - - /** `get` keyword */ - GET("get"), - - /** `set` keyword */ - SET("set"), - - /** `dynamic` keyword */ - DYNAMIC("dynamic"), - - /** `file` keyword (annotation target) */ - FILE("file"), - - /** `field` keyword (annotation target/backing field) */ - FIELD("field"), - - /** `property` keyword (annotation target) */ - PROPERTY("property"), - - /** `receiver` keyword (annotation target) */ - RECEIVER("receiver"), - - /** `param` keyword (annotation target) */ - PARAM("param"), - - /** `setparam` keyword (annotation target) */ - SETPARAM("setparam"), - - /** `delegate` keyword (annotation target) */ - DELEGATE("delegate"), - - /** `public` keyword */ - PUBLIC("public"), - - /** `private` keyword */ - PRIVATE("private"), - - /** `protected` keyword */ - PROTECTED("protected"), - - /** `internal` keyword */ - INTERNAL("internal"), - - /** `enum` keyword */ - ENUM("enum"), - - /** `sealed` keyword */ - SEALED("sealed"), - - /** `data` keyword */ - DATA("data"), - - /** `inner` keyword */ - INNER("inner"), - - /** `value` keyword (value class) */ - VALUE("value"), - - /** `tailrec` keyword */ - TAILREC("tailrec"), - - /** `operator` keyword */ - OPERATOR("operator"), - - /** `inline` keyword */ - INLINE("inline"), - - /** `infix` keyword */ - INFIX("infix"), - - /** `external` keyword */ - EXTERNAL("external"), - - /** `suspend` keyword */ - SUSPEND("suspend"), - - /** `override` keyword */ - OVERRIDE("override"), - - /** `abstract` keyword */ - ABSTRACT("abstract"), - - /** `final` keyword */ - FINAL("final"), - - /** `open` keyword */ - OPEN("open"), - - /** `const` keyword */ - CONST("const"), - - /** `lateinit` keyword */ - LATEINIT("lateinit"), - - /** `vararg` keyword */ - VARARG("vararg"), - - /** `noinline` keyword */ - NOINLINE("noinline"), - - /** `crossinline` keyword */ - CROSSINLINE("crossinline"), - - /** `reified` keyword */ - REIFIED("reified"), - - /** `expect` keyword (multiplatform) */ - EXPECT("expect"), - - /** `actual` keyword (multiplatform) */ - ACTUAL("actual"), - - // ═══════════════════════════════════════════════════════════════════════════════ - // Special - // ═══════════════════════════════════════════════════════════════════════════════ - - /** Comment (line or block) */ - COMMENT("comment"), - - /** Line comment: `// ...` */ - LINE_COMMENT("line_comment"), - - /** Multiline comment: `/* ... */` */ - MULTILINE_COMMENT("multiline_comment"), - - /** Shebang line: `#!/usr/bin/env kotlin` */ - SHEBANG_LINE("shebang_line"), - - /** Error node (for parse errors) */ - ERROR("ERROR"), - - /** Unknown/unrecognized node type */ - UNKNOWN("unknown"); - - companion object { - /** Lookup table for O(1) conversion from tree-sitter names */ - private val byTreeSitterName: Map = buildMap { - SyntaxKind.entries.forEach { kind -> put(kind.treeSitterName, kind) } - put("null", NULL_LITERAL) - put("true", BOOLEAN_LITERAL) - put("false", BOOLEAN_LITERAL) - put(":", COLON) - put("=", ASSIGNMENT_OPERATOR) - put("==", EQUALITY_OPERATOR) - put("!=", EQUALITY_OPERATOR) - put("===", EQUALITY_OPERATOR) - put("!==", EQUALITY_OPERATOR) - put("+", ADDITIVE_OPERATOR) - put("-", ADDITIVE_OPERATOR) - put("/", MULTIPLICATIVE_OPERATOR) - put("%", MULTIPLICATIVE_OPERATOR) - put("<=", COMPARISON_OPERATOR) - put(">=", COMPARISON_OPERATOR) - } - - /** - * Converts a tree-sitter node type string to a [SyntaxKind]. - * - * @param name The tree-sitter node type string (e.g., "function_declaration") - * @return The corresponding [SyntaxKind], or [UNKNOWN] if not recognized - */ - fun fromTreeSitter(name: String): SyntaxKind = byTreeSitterName[name] ?: UNKNOWN - - /** - * Checks if a node kind represents a declaration. - * - * Declarations introduce named entities into scopes: classes, functions, properties, etc. - */ - fun isDeclaration(kind: SyntaxKind): Boolean = kind in DECLARATIONS - - /** - * Checks if a node kind represents an expression. - * - * Expressions are code that evaluates to a value. - */ - fun isExpression(kind: SyntaxKind): Boolean = kind in EXPRESSIONS - - /** - * Checks if a node kind represents a statement. - * - * Statements are executable code that may have side effects. - */ - fun isStatement(kind: SyntaxKind): Boolean = kind in STATEMENTS - - /** - * Checks if a node kind represents a type reference. - */ - fun isType(kind: SyntaxKind): Boolean = kind in TYPES - - /** - * Checks if a node kind represents a literal value. - */ - fun isLiteral(kind: SyntaxKind): Boolean = kind in LITERALS - - /** - * Checks if a node kind represents a modifier. - */ - fun isModifier(kind: SyntaxKind): Boolean = kind in MODIFIER_KINDS - - /** All declaration node kinds */ - private val DECLARATIONS = setOf( - CLASS_DECLARATION, - OBJECT_DECLARATION, - INTERFACE_DECLARATION, - FUNCTION_DECLARATION, - PROPERTY_DECLARATION, - TYPE_ALIAS, - ENUM_ENTRY, - SECONDARY_CONSTRUCTOR, - PARAMETER, - CLASS_PARAMETER, - TYPE_PARAMETER, - COMPANION_OBJECT, - ANONYMOUS_INITIALIZER - ) - - /** All expression node kinds */ - private val EXPRESSIONS = setOf( - PARENTHESIZED_EXPRESSION, - COLLECTION_LITERAL, - THIS_EXPRESSION, - SUPER_EXPRESSION, - IF_EXPRESSION, - WHEN_EXPRESSION, - TRY_EXPRESSION, - JUMP_EXPRESSION, - OBJECT_LITERAL, - LAMBDA_LITERAL, - ANONYMOUS_FUNCTION, - CALL_EXPRESSION, - INDEXING_EXPRESSION, - NAVIGATION_EXPRESSION, - PREFIX_EXPRESSION, - POSTFIX_EXPRESSION, - AS_EXPRESSION, - SPREAD_EXPRESSION, - BINARY_EXPRESSION, - INFIX_EXPRESSION, - ELVIS_EXPRESSION, - CHECK_EXPRESSION, - COMPARISON_EXPRESSION, - EQUALITY_EXPRESSION, - CONJUNCTION_EXPRESSION, - DISJUNCTION_EXPRESSION, - ADDITIVE_EXPRESSION, - MULTIPLICATIVE_EXPRESSION, - RANGE_EXPRESSION, - ASSIGNMENT, - AUGMENTED_ASSIGNMENT, - STRING_LITERAL, - LINE_STRING_LITERAL, - MULTI_LINE_STRING_LITERAL, - INTEGER_LITERAL, - LONG_LITERAL, - HEX_LITERAL, - BIN_LITERAL, - REAL_LITERAL, - BOOLEAN_LITERAL, - CHARACTER_LITERAL, - NULL_LITERAL, - SIMPLE_IDENTIFIER - ) - - /** All statement node kinds */ - private val STATEMENTS = setOf( - FOR_STATEMENT, - WHILE_STATEMENT, - DO_WHILE_STATEMENT, - ASSIGNMENT, - AUGMENTED_ASSIGNMENT - ) - - /** All type node kinds */ - private val TYPES = setOf( - USER_TYPE, - SIMPLE_USER_TYPE, - NULLABLE_TYPE, - PARENTHESIZED_TYPE, - FUNCTION_TYPE, - DYNAMIC_TYPE, - RECEIVER_TYPE - ) - - /** All literal node kinds */ - private val LITERALS = setOf( - INTEGER_LITERAL, - LONG_LITERAL, - HEX_LITERAL, - BIN_LITERAL, - REAL_LITERAL, - BOOLEAN_LITERAL, - CHARACTER_LITERAL, - NULL_LITERAL, - STRING_LITERAL, - LINE_STRING_LITERAL, - MULTI_LINE_STRING_LITERAL - ) - - /** All modifier node kinds */ - private val MODIFIER_KINDS: Set = setOf( - SyntaxKind.MODIFIERS, - MODIFIER, - CLASS_MODIFIER, - MEMBER_MODIFIER, - VISIBILITY_MODIFIER, - VARIANCE_MODIFIER, - TYPE_PARAMETER_MODIFIER, - FUNCTION_MODIFIER, - PROPERTY_MODIFIER, - INHERITANCE_MODIFIER, - PARAMETER_MODIFIER, - PLATFORM_MODIFIER - ) - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxNode.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxNode.kt deleted file mode 100644 index 2c1a91b1ad..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxNode.kt +++ /dev/null @@ -1,519 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -import com.itsaky.androidide.treesitter.TSNode - -/** - * Immutable wrapper around a tree-sitter syntax node. - * - * This class provides a Kotlin-idiomatic API for navigating and querying syntax trees. - * It wraps the tree-sitter [TSNode] to provide: - * - Type-safe node kinds via [SyntaxKind] - * - Cached text extraction - * - Convenient navigation methods - * - Position/range utilities - * - * ## Design Philosophy - * - * This wrapper exists to: - * 1. **Isolate tree-sitter**: Changes to tree-sitter API only affect this class - * 2. **Add type safety**: [SyntaxKind] enum instead of raw strings - * 3. **Improve ergonomics**: Kotlin idioms like sequences and null safety - * 4. **Enable caching**: Text and children can be cached for repeated access - * - * ## Memory Model - * - * SyntaxNode instances are lightweight wrappers. The underlying tree-sitter [TSNode] - * references data in the [SyntaxTree], which must remain alive while nodes are used. - * Child nodes are created lazily when accessed. - * - * ## Example - * - * ```kotlin - * val root = tree.root - * val functions = root.children - * .filter { it.kind == SyntaxKind.FUNCTION_DECLARATION } - * .map { it.childByFieldName("name")?.text } - * ``` - * - * @property node The underlying tree-sitter node - * @property source The complete source text (for text extraction) - */ -class SyntaxNode internal constructor( - internal val node: TSNode, - private val source: String -) { - companion object { - private fun TSNode.isNullOrInvalid(): Boolean { - return try { - this.isNull - } catch (e: IllegalStateException) { - true - } - } - } - /** - * The type of this syntax node as a [SyntaxKind]. - * - * Unknown tree-sitter types map to [SyntaxKind.UNKNOWN]. - */ - val kind: SyntaxKind by lazy { - try { - SyntaxKind.fromTreeSitter(node.type) - } catch (e: IllegalStateException) { - SyntaxKind.UNKNOWN - } - } - - /** - * The raw tree-sitter type string. - * - * Use [kind] for type-safe access. This property is useful for debugging - * or handling nodes not yet in [SyntaxKind]. - */ - val type: String - get() = try { node.type } catch (e: IllegalStateException) { "ERROR" } - - /** - * The source text covered by this node. - * - * AndroidIDE's tree-sitter uses UTF-16 byte offsets (2 bytes per BMP char). - * We use PositionConverter to properly handle the conversion. - */ - val text: String by lazy { - try { - PositionConverter.extractSubstring(source, node.startByte, node.endByte) - } catch (e: Exception) { - "" - } - } - - /** - * The text range (start and end positions) of this node. - * - * Uses tree-sitter's row directly and divides column by 2 - * to convert UTF-16 byte offsets to character positions. - * Also includes character offsets for accurate index calculation. - */ - val range: TextRange by lazy { - try { - TextRange( - start = Position( - line = node.startPoint.row, - column = node.startPoint.column / 2 - ), - end = Position( - line = node.endPoint.row, - column = node.endPoint.column / 2 - ), - startOffset = startOffset, - endOffset = endOffset - ) - } catch (e: IllegalStateException) { - TextRange(Position(0, 0), Position(0, 0)) - } - } - - /** Zero-based start line */ - val startLine: Int - get() = try { node.startPoint.row } catch (e: IllegalStateException) { 0 } - - /** Zero-based end line */ - val endLine: Int - get() = try { node.endPoint.row } catch (e: IllegalStateException) { 0 } - - /** Zero-based start column (UTF-16 bytes / 2 for character position) */ - val startColumn: Int - get() = try { node.startPoint.column / 2 } catch (e: IllegalStateException) { 0 } - - /** Zero-based end column (UTF-16 bytes / 2 for character position) */ - val endColumn: Int - get() = try { node.endPoint.column / 2 } catch (e: IllegalStateException) { 0 } - - /** Byte offset of start position in source (raw from tree-sitter, UTF-16 based) */ - val startByte: Int - get() = try { node.startByte } catch (e: IllegalStateException) { 0 } - - /** Byte offset of end position in source (raw from tree-sitter, UTF-16 based) */ - val endByte: Int - get() = try { node.endByte } catch (e: IllegalStateException) { 0 } - - /** Character offset of start position in source */ - val startOffset: Int - get() = PositionConverter.byteOffsetToCharIndex(source, startByte) - - /** Character offset of end position in source */ - val endOffset: Int - get() = PositionConverter.byteOffsetToCharIndex(source, endByte) - - /** - * The parent node, or null if this is the root. - */ - val parent: SyntaxNode? - get() { - val p = node.parent - return if (p.isNullOrInvalid()) null else SyntaxNode(p, source) - } - - /** - * All child nodes, including anonymous (punctuation, operators) nodes. - * - * Use [namedChildren] if you only want significant syntax elements. - */ - val children: List by lazy { - try { - (0 until node.childCount).mapNotNull { index -> - try { - val child = node.getChild(index) - if (child.isNullOrInvalid()) null else SyntaxNode(child, source) - } catch (e: IllegalStateException) { - null - } - } - } catch (e: IllegalStateException) { - emptyList() - } - } - - /** - * Only "named" child nodes (excludes punctuation and anonymous nodes). - * - * Named nodes are significant syntax elements like declarations, - * expressions, and identifiers. Unnamed nodes are punctuation, - * operators, and other grammar artifacts. - */ - val namedChildren: List by lazy { - try { - (0 until node.namedChildCount).mapNotNull { index -> - try { - val child = node.getNamedChild(index) - if (child.isNullOrInvalid()) null else SyntaxNode(child, source) - } catch (e: IllegalStateException) { - null - } - } - } catch (e: IllegalStateException) { - emptyList() - } - } - - /** Total number of children (including anonymous) */ - val childCount: Int - get() = try { node.childCount } catch (e: IllegalStateException) { 0 } - - /** Number of named children */ - val namedChildCount: Int - get() = try { node.namedChildCount } catch (e: IllegalStateException) { 0 } - - /** - * Whether this is a "named" node (significant syntax element). - * - * Named nodes represent meaningful language constructs. - * Anonymous nodes are grammar artifacts like punctuation. - */ - val isNamed: Boolean - get() = try { node.isNamed } catch (e: IllegalStateException) { false } - - /** - * Whether this node represents a syntax error. - */ - val isError: Boolean - get() = try { node.isError } catch (e: IllegalStateException) { true } - - /** - * Whether this node is missing from the source (inserted by error recovery). - * - * Tree-sitter may insert missing nodes to produce a valid parse tree - * when the source has syntax errors. - */ - val isMissing: Boolean - get() = try { node.isMissing } catch (e: IllegalStateException) { true } - - /** - * Whether this node or any descendant contains a syntax error. - */ - val hasError: Boolean - get() = try { node.hasErrors() } catch (e: IllegalStateException) { true } - - /** - * The next sibling node (same parent), or null if last child. - */ - val nextSibling: SyntaxNode? - get() { - val sibling = node.nextSibling - return if (sibling.isNullOrInvalid()) null else SyntaxNode(sibling, source) - } - - /** - * The previous sibling node, or null if first child. - */ - val prevSibling: SyntaxNode? - get() { - val sibling = node.getPreviousSibling() - return if (sibling.isNullOrInvalid()) null else SyntaxNode(sibling, source) - } - - /** - * The next named sibling, or null if no more named siblings. - */ - val nextNamedSibling: SyntaxNode? - get() { - val sibling = node.nextNamedSibling - return if (sibling.isNullOrInvalid()) null else SyntaxNode(sibling, source) - } - - /** - * The previous named sibling, or null if no previous named siblings. - */ - val prevNamedSibling: SyntaxNode? - get() { - val sibling = node.getPreviousNamedSibling() - return if (sibling.isNullOrInvalid()) null else SyntaxNode(sibling, source) - } - - /** - * The first child node, or null if no children. - */ - val firstChild: SyntaxNode? - get() = if (childCount > 0) child(0) else null - - /** - * The last child node, or null if no children. - */ - val lastChild: SyntaxNode? - get() = if (childCount > 0) child(childCount - 1) else null - - /** - * The first named child, or null if no named children. - */ - val firstNamedChild: SyntaxNode? - get() = if (namedChildCount > 0) namedChild(0) else null - - /** - * The last named child, or null if no named children. - */ - val lastNamedChild: SyntaxNode? - get() = if (namedChildCount > 0) namedChild(namedChildCount - 1) else null - - /** - * Gets a child by index. - * - * @param index Zero-based child index - * @return The child node, or null if index is out of bounds - */ - fun child(index: Int): SyntaxNode? { - return try { - if (index in 0 until childCount) { - val child = node.getChild(index) - if (child.isNullOrInvalid()) null else SyntaxNode(child, source) - } else { - null - } - } catch (e: IllegalStateException) { - null - } - } - - /** - * Gets a named child by index. - * - * @param index Zero-based index among named children only - * @return The named child, or null if index is out of bounds - */ - fun namedChild(index: Int): SyntaxNode? { - return try { - if (index in 0 until namedChildCount) { - val child = node.getNamedChild(index) - if (child.isNullOrInvalid()) null else SyntaxNode(child, source) - } else { - null - } - } catch (e: IllegalStateException) { - null - } - } - - /** - * Gets a child by its field name in the grammar. - * - * Field names provide semantic access to node parts. For example, - * a function declaration has fields like "name", "parameters", "body". - * - * @param fieldName The grammar field name - * @return The child at that field, or null if not present - */ - fun childByFieldName(fieldName: String): SyntaxNode? { - return try { - val child = node.getChildByFieldName(fieldName) - if (child.isNullOrInvalid()) null else SyntaxNode(child, source) - } catch (e: IllegalStateException) { - null - } - } - - /** - * Finds the first child with the given kind. - * - * @param kind The [SyntaxKind] to find - * @return The first matching child, or null if not found - */ - fun findChild(kind: SyntaxKind): SyntaxNode? { - return children.find { it.kind == kind } - } - - /** - * Finds all children with the given kind. - * - * @param kind The [SyntaxKind] to find - * @return List of all matching children - */ - fun findChildren(kind: SyntaxKind): List { - return children.filter { it.kind == kind } - } - - /** - * Finds all descendants matching a predicate. - * - * Traverses the tree depth-first. - * - * @param predicate Function that tests each node - * @return Sequence of matching nodes - */ - fun findAll(predicate: (SyntaxNode) -> Boolean): Sequence = sequence { - if (predicate(this@SyntaxNode)) { - yield(this@SyntaxNode) - } - for (child in children) { - yieldAll(child.findAll(predicate)) - } - } - - /** - * Finds all descendants of a specific kind. - * - * @param kind The [SyntaxKind] to find - * @return Sequence of all descendants with matching kind - */ - fun findAllByKind(kind: SyntaxKind): Sequence { - return findAll { it.kind == kind } - } - - /** - * Finds the deepest node containing the given position. - * - * @param position The position to find - * @return The deepest node containing that position, or null if outside - */ - fun nodeAtPosition(position: Position): SyntaxNode? { - if (position !in range) return null - - for (child in children) { - val found = child.nodeAtPosition(position) - if (found != null) return found - } - return this - } - - /** - * Finds the deepest named node containing the given position. - * - * This is useful for finding meaningful syntax elements at a cursor position. - * - * @param position The position to find - * @return The deepest named node at that position, or null if outside - */ - fun namedNodeAtPosition(position: Position): SyntaxNode? { - if (position !in range) return null - - for (child in namedChildren) { - val found = child.namedNodeAtPosition(position) - if (found != null) return found - } - return if (isNamed) this else null - } - - /** - * Walks up the tree to find the first ancestor matching a predicate. - * - * @param predicate Function that tests each ancestor - * @return The first matching ancestor, or null if none match - */ - fun findAncestor(predicate: (SyntaxNode) -> Boolean): SyntaxNode? { - var current = parent - while (current != null) { - if (predicate(current)) return current - current = current.parent - } - return null - } - - /** - * Walks up the tree to find the first ancestor of a specific kind. - * - * @param kind The [SyntaxKind] to find - * @return The first ancestor with that kind, or null if not found - */ - fun findAncestorByKind(kind: SyntaxKind): SyntaxNode? { - return findAncestor { it.kind == kind } - } - - /** - * Gets the sequence of ancestors from this node to the root. - */ - val ancestors: Sequence - get() = sequence { - var current = parent - while (current != null) { - yield(current) - current = current.parent - } - } - - /** - * Traverses this node and all descendants depth-first. - * - * @return Sequence of all nodes in depth-first order - */ - fun traverse(): Sequence = sequence { - yield(this@SyntaxNode) - for (child in children) { - yieldAll(child.traverse()) - } - } - - /** - * Traverses only named nodes depth-first. - * - * @return Sequence of named nodes in depth-first order - */ - fun traverseNamed(): Sequence = sequence { - if (isNamed) yield(this@SyntaxNode) - for (child in namedChildren) { - yieldAll(child.traverseNamed()) - } - } - - /** - * Returns the S-expression representation of this subtree. - * - * S-expressions are a compact textual representation of the tree structure, - * useful for debugging and testing. - */ - fun toSexp(): String = try { node.nodeString } catch (e: IllegalStateException) { "(error)" } - - /** - * Checks if this node's position contains another position. - */ - operator fun contains(position: Position): Boolean = position in range - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is SyntaxNode) return false - return node == other.node - } - - override fun hashCode(): Int = node.hashCode() - - override fun toString(): String { - return "SyntaxNode(kind=$kind, range=$range, text='${text.take(50)}${if (text.length > 50) "..." else ""}')" - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxTree.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxTree.kt deleted file mode 100644 index dc23b06c73..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxTree.kt +++ /dev/null @@ -1,220 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -import com.itsaky.androidide.treesitter.TSTree -import java.io.Closeable - -/** - * Immutable wrapper around a tree-sitter parse tree. - * - * SyntaxTree represents a complete parsed Kotlin source file. It holds: - * - The parsed tree structure - * - The original source text (for node text extraction) - * - Metadata about parse errors - * - * ## Lifecycle Management - * - * Tree-sitter trees allocate native memory. This class implements [Closeable] - * to ensure proper cleanup: - * - * ```kotlin - * parser.parse(source).tree.use { tree -> - * // Use tree... - * } // Tree is automatically closed - * ``` - * - * For incremental parsing, the old tree must remain open until the new tree is created: - * - * ```kotlin - * val tree1 = parser.parse(source1) - * val tree2 = parser.parseIncremental(source2, tree1.tree, edit) - * tree1.tree.close() // Now safe to close - * ``` - * - * ## Immutability - * - * Once created, a SyntaxTree cannot be modified. To get a tree reflecting edits, - * use [KotlinParser.parseIncremental] which creates a new tree. - * - * ## Thread Safety - * - * Reading from a SyntaxTree is thread-safe. Multiple threads can navigate - * the same tree concurrently. However, trees should not be shared across - * threads if using incremental parsing, as tree-sitter's edit tracking - * is not thread-safe. - * - * @property tree The underlying tree-sitter tree - * @property source The original source text - * @property languageVersion The tree-sitter language version used - */ -class SyntaxTree internal constructor( - internal val tree: TSTree, - private val source: String, - val languageVersion: Int = 0 -) : Closeable { - - /** - * The root node of the syntax tree. - * - * For a well-formed Kotlin file, this will be a SOURCE_FILE node. - */ - val root: SyntaxNode by lazy { - SyntaxNode(tree.rootNode, source) - } - - /** - * The original source text that was parsed. - */ - val sourceText: String get() = source - - /** - * The length of the source text in bytes. - */ - val sourceLength: Int get() = source.length - - /** - * Whether this tree contains any syntax errors. - * - * Even with errors, tree-sitter produces a "best effort" tree. - * Error nodes are marked with [SyntaxNode.isError]. - */ - val hasErrors: Boolean by lazy { - root.hasError - } - - /** - * Collects all error nodes in the tree. - * - * Error nodes represent portions of the source that couldn't be parsed - * according to the grammar. Each error node has a [SyntaxNode.range] - * indicating where the error occurred. - * - * @return List of error nodes in document order - */ - val errors: List by lazy { - root.findAll { it.isError || it.isMissing }.toList() - } - - /** - * Finds the deepest node at a given position. - * - * @param position The position to query - * @return The deepest node containing that position, or null if outside tree - */ - fun nodeAtPosition(position: Position): SyntaxNode? { - return root.nodeAtPosition(position) - } - - /** - * Finds the deepest named node at a given position. - * - * Named nodes represent meaningful syntax elements (not punctuation). - * - * @param position The position to query - * @return The deepest named node at that position - */ - fun namedNodeAtPosition(position: Position): SyntaxNode? { - return root.namedNodeAtPosition(position) - } - - /** - * Gets all nodes that overlap with a given range. - * - * @param range The range to query - * @return Sequence of nodes overlapping the range - */ - fun nodesInRange(range: TextRange): Sequence { - return root.findAll { it.range.overlaps(range) } - } - - /** - * Converts a byte offset to a [Position]. - * - * @param byteOffset Byte offset in the source - * @return Position (line and column) - */ - fun positionFromOffset(byteOffset: Int): Position { - require(byteOffset >= 0) { "Byte offset must be non-negative" } - require(byteOffset <= source.length) { "Byte offset $byteOffset exceeds source length ${source.length}" } - - var line = 0 - var lineStart = 0 - - for (i in 0 until byteOffset) { - if (i < source.length && source[i] == '\n') { - line++ - lineStart = i + 1 - } - } - - return Position(line = line, column = byteOffset - lineStart) - } - - /** - * Converts a [Position] to a byte offset. - * - * @param position The position (line and column) - * @return Byte offset in the source - */ - fun offsetFromPosition(position: Position): Int { - var offset = 0 - var currentLine = 0 - - while (currentLine < position.line && offset < source.length) { - if (source[offset] == '\n') { - currentLine++ - } - offset++ - } - - return offset + position.column - } - - /** - * Extracts a substring from the source using a [TextRange]. - * - * @param range The range to extract - * @return The source text in that range - */ - fun textInRange(range: TextRange): String { - val startOffset = offsetFromPosition(range.start) - val endOffset = offsetFromPosition(range.end) - return source.substring( - startOffset.coerceIn(0, source.length), - endOffset.coerceIn(0, source.length) - ) - } - - /** - * Returns the S-expression representation of the entire tree. - * - * Useful for debugging and testing. - */ - fun toSexp(): String = root.toSexp() - - /** - * Creates a copy of this tree for concurrent access. - * - * Tree-sitter trees can be copied for use in different threads. - * The copy shares structure with the original but can be - * accessed independently. - * - * @return A new SyntaxTree backed by a copied tree - */ - fun copy(): SyntaxTree { - return SyntaxTree(tree.copy(), source, languageVersion) - } - - /** - * Releases native resources held by this tree. - * - * After calling close(), the tree should not be used. - * Accessing nodes from a closed tree results in undefined behavior. - */ - override fun close() { - tree.close() - } - - override fun toString(): String { - return "SyntaxTree(hasErrors=$hasErrors, lines=${root.endLine + 1})" - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxVisitor.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxVisitor.kt deleted file mode 100644 index a8e199ca49..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/SyntaxVisitor.kt +++ /dev/null @@ -1,492 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -/** - * Visitor interface for traversing Kotlin syntax trees. - * - * Implements the Visitor design pattern for syntax tree traversal. - * Each visit method corresponds to a category of syntax nodes. - * - * ## Design Philosophy - * - * This visitor groups related node types into categories rather than - * having a method per [SyntaxKind]. This provides: - * 1. A manageable API (not 200+ methods) - * 2. Logical groupings that match how you typically process code - * 3. Flexibility to handle new node types in catch-all methods - * - * ## Usage - * - * Extend [SyntaxVisitorBase] for a default implementation that visits all children: - * - * ```kotlin - * class FunctionFinder : SyntaxVisitorBase>() { - * private val functions = mutableListOf() - * - * override fun visitFunctionDeclaration(node: SyntaxNode): List { - * val name = node.childByFieldName("name")?.text - * if (name != null) functions.add(name) - * visitChildren(node) - * return functions - * } - * - * override fun defaultResult(): List = functions - * } - * - * val finder = FunctionFinder() - * val names = finder.visit(tree.root) - * ``` - * - * ## Traversal Control - * - * To control traversal, override visit methods and choose whether to call - * [visitChildren]. Not calling it skips the subtree. - * - * @param R The return type of visit methods - */ -interface SyntaxVisitor { - - /** - * Main entry point for visiting a node. - * - * Dispatches to the appropriate visit method based on node kind. - * - * @param node The node to visit - * @return Result of visiting the node - */ - fun visit(node: SyntaxNode): R - - /** - * Visits all children of a node in order. - * - * @param node The parent node whose children to visit - * @return Result after visiting all children - */ - fun visitChildren(node: SyntaxNode): R - - /** - * The default result when no specific handling is needed. - */ - fun defaultResult(): R - - /** - * Combines results from visiting multiple nodes. - * - * @param aggregate The accumulated result so far - * @param nextResult The result from the latest visit - * @return Combined result - */ - fun aggregateResult(aggregate: R, nextResult: R): R - - /** - * Called for the root source_file node. - */ - fun visitSourceFile(node: SyntaxNode): R - - /** - * Called for package declarations. - */ - fun visitPackageHeader(node: SyntaxNode): R - - /** - * Called for import statements. - */ - fun visitImport(node: SyntaxNode): R - - /** - * Called for class declarations (class, interface, object, enum). - */ - fun visitClassDeclaration(node: SyntaxNode): R - - /** - * Called for object declarations. - */ - fun visitObjectDeclaration(node: SyntaxNode): R - - /** - * Called for function declarations. - */ - fun visitFunctionDeclaration(node: SyntaxNode): R - - /** - * Called for property declarations (val/var). - */ - fun visitPropertyDeclaration(node: SyntaxNode): R - - /** - * Called for type alias declarations. - */ - fun visitTypeAlias(node: SyntaxNode): R - - /** - * Called for parameters (function, constructor, lambda). - */ - fun visitParameter(node: SyntaxNode): R - - /** - * Called for type parameter declarations (). - */ - fun visitTypeParameter(node: SyntaxNode): R - - /** - * Called for type references (user types, function types, etc.). - */ - fun visitType(node: SyntaxNode): R - - /** - * Called for block statements ({ ... }). - */ - fun visitBlock(node: SyntaxNode): R - - /** - * Called for if expressions. - */ - fun visitIfExpression(node: SyntaxNode): R - - /** - * Called for when expressions. - */ - fun visitWhenExpression(node: SyntaxNode): R - - /** - * Called for try expressions. - */ - fun visitTryExpression(node: SyntaxNode): R - - /** - * Called for loop statements (for, while, do-while). - */ - fun visitLoop(node: SyntaxNode): R - - /** - * Called for function/method calls. - */ - fun visitCallExpression(node: SyntaxNode): R - - /** - * Called for navigation expressions (member access: a.b). - */ - fun visitNavigationExpression(node: SyntaxNode): R - - /** - * Called for indexing expressions (a[b]). - */ - fun visitIndexingExpression(node: SyntaxNode): R - - /** - * Called for binary expressions (a + b, a == b, etc.). - */ - fun visitBinaryExpression(node: SyntaxNode): R - - /** - * Called for unary expressions (prefix and postfix). - */ - fun visitUnaryExpression(node: SyntaxNode): R - - /** - * Called for lambda expressions. - */ - fun visitLambdaExpression(node: SyntaxNode): R - - /** - * Called for object literals (anonymous objects). - */ - fun visitObjectLiteral(node: SyntaxNode): R - - /** - * Called for this/super expressions. - */ - fun visitThisOrSuperExpression(node: SyntaxNode): R - - /** - * Called for jump expressions (return, break, continue, throw). - */ - fun visitJumpExpression(node: SyntaxNode): R - - /** - * Called for literal values (numbers, strings, booleans, null). - */ - fun visitLiteral(node: SyntaxNode): R - - /** - * Called for identifiers (simple_identifier, type_identifier). - */ - fun visitIdentifier(node: SyntaxNode): R - - /** - * Called for string templates and interpolations. - */ - fun visitStringTemplate(node: SyntaxNode): R - - /** - * Called for annotations. - */ - fun visitAnnotation(node: SyntaxNode): R - - /** - * Called for modifiers (visibility, inheritance, etc.). - */ - fun visitModifier(node: SyntaxNode): R - - /** - * Called for any node not handled by specific methods. - */ - fun visitOther(node: SyntaxNode): R -} - -/** - * Base implementation of [SyntaxVisitor] with default behavior. - * - * By default, all visit methods call [visitChildren] to continue traversal. - * Override specific methods to customize handling for those node types. - * - * @param R The return type of visit methods - */ -abstract class SyntaxVisitorBase : SyntaxVisitor { - - override fun visit(node: SyntaxNode): R { - return when (node.kind) { - SyntaxKind.SOURCE_FILE -> visitSourceFile(node) - SyntaxKind.PACKAGE_HEADER -> visitPackageHeader(node) - - SyntaxKind.IMPORT_LIST, - SyntaxKind.IMPORT_HEADER -> visitImport(node) - - SyntaxKind.CLASS_DECLARATION, - SyntaxKind.INTERFACE_DECLARATION -> visitClassDeclaration(node) - - SyntaxKind.OBJECT_DECLARATION, - SyntaxKind.COMPANION_OBJECT -> visitObjectDeclaration(node) - - SyntaxKind.FUNCTION_DECLARATION, - SyntaxKind.ANONYMOUS_FUNCTION, - SyntaxKind.SECONDARY_CONSTRUCTOR -> visitFunctionDeclaration(node) - - SyntaxKind.PROPERTY_DECLARATION -> visitPropertyDeclaration(node) - SyntaxKind.TYPE_ALIAS -> visitTypeAlias(node) - - SyntaxKind.PARAMETER, - SyntaxKind.CLASS_PARAMETER, - SyntaxKind.PARAMETER_WITH_OPTIONAL_TYPE -> visitParameter(node) - - SyntaxKind.TYPE_PARAMETER -> visitTypeParameter(node) - - SyntaxKind.USER_TYPE, - SyntaxKind.SIMPLE_USER_TYPE, - SyntaxKind.NULLABLE_TYPE, - SyntaxKind.FUNCTION_TYPE, - SyntaxKind.PARENTHESIZED_TYPE, - SyntaxKind.DYNAMIC_TYPE, - SyntaxKind.RECEIVER_TYPE -> visitType(node) - - SyntaxKind.CLASS_BODY, - SyntaxKind.CONTROL_STRUCTURE_BODY, - SyntaxKind.STATEMENTS -> visitBlock(node) - - SyntaxKind.IF_EXPRESSION -> visitIfExpression(node) - SyntaxKind.WHEN_EXPRESSION -> visitWhenExpression(node) - SyntaxKind.TRY_EXPRESSION -> visitTryExpression(node) - - SyntaxKind.FOR_STATEMENT, - SyntaxKind.WHILE_STATEMENT, - SyntaxKind.DO_WHILE_STATEMENT -> visitLoop(node) - - SyntaxKind.CALL_EXPRESSION -> visitCallExpression(node) - SyntaxKind.NAVIGATION_EXPRESSION -> visitNavigationExpression(node) - SyntaxKind.INDEXING_EXPRESSION -> visitIndexingExpression(node) - - SyntaxKind.BINARY_EXPRESSION, - SyntaxKind.INFIX_EXPRESSION, - SyntaxKind.COMPARISON_EXPRESSION, - SyntaxKind.EQUALITY_EXPRESSION, - SyntaxKind.CONJUNCTION_EXPRESSION, - SyntaxKind.DISJUNCTION_EXPRESSION, - SyntaxKind.ADDITIVE_EXPRESSION, - SyntaxKind.MULTIPLICATIVE_EXPRESSION, - SyntaxKind.RANGE_EXPRESSION, - SyntaxKind.ELVIS_EXPRESSION, - SyntaxKind.CHECK_EXPRESSION, - SyntaxKind.AS_EXPRESSION, - SyntaxKind.ASSIGNMENT, - SyntaxKind.AUGMENTED_ASSIGNMENT -> visitBinaryExpression(node) - - SyntaxKind.PREFIX_EXPRESSION, - SyntaxKind.POSTFIX_EXPRESSION, - SyntaxKind.SPREAD_EXPRESSION -> visitUnaryExpression(node) - - SyntaxKind.LAMBDA_LITERAL, - SyntaxKind.ANNOTATED_LAMBDA -> visitLambdaExpression(node) - - SyntaxKind.OBJECT_LITERAL -> visitObjectLiteral(node) - - SyntaxKind.THIS_EXPRESSION, - SyntaxKind.SUPER_EXPRESSION -> visitThisOrSuperExpression(node) - - SyntaxKind.JUMP_EXPRESSION -> visitJumpExpression(node) - - SyntaxKind.INTEGER_LITERAL, - SyntaxKind.LONG_LITERAL, - SyntaxKind.HEX_LITERAL, - SyntaxKind.BIN_LITERAL, - SyntaxKind.REAL_LITERAL, - SyntaxKind.BOOLEAN_LITERAL, - SyntaxKind.CHARACTER_LITERAL, - SyntaxKind.NULL_LITERAL -> visitLiteral(node) - - SyntaxKind.SIMPLE_IDENTIFIER, - SyntaxKind.TYPE_IDENTIFIER, - SyntaxKind.IDENTIFIER -> visitIdentifier(node) - - SyntaxKind.STRING_LITERAL, - SyntaxKind.LINE_STRING_LITERAL, - SyntaxKind.MULTI_LINE_STRING_LITERAL, - SyntaxKind.INTERPOLATION, - SyntaxKind.LINE_STRING_EXPRESSION, - SyntaxKind.MULTI_LINE_STRING_EXPRESSION -> visitStringTemplate(node) - - SyntaxKind.ANNOTATION, - SyntaxKind.SINGLE_ANNOTATION, - SyntaxKind.MULTI_ANNOTATION, - SyntaxKind.FILE_ANNOTATION -> visitAnnotation(node) - - SyntaxKind.MODIFIERS, - SyntaxKind.MODIFIER, - SyntaxKind.VISIBILITY_MODIFIER, - SyntaxKind.INHERITANCE_MODIFIER, - SyntaxKind.CLASS_MODIFIER, - SyntaxKind.MEMBER_MODIFIER, - SyntaxKind.FUNCTION_MODIFIER, - SyntaxKind.PROPERTY_MODIFIER, - SyntaxKind.PARAMETER_MODIFIER, - SyntaxKind.PLATFORM_MODIFIER, - SyntaxKind.VARIANCE_MODIFIER, - SyntaxKind.TYPE_PARAMETER_MODIFIER -> visitModifier(node) - - else -> visitOther(node) - } - } - - override fun visitChildren(node: SyntaxNode): R { - var result = defaultResult() - for (child in node.namedChildren) { - val childResult = visit(child) - result = aggregateResult(result, childResult) - } - return result - } - - override fun aggregateResult(aggregate: R, nextResult: R): R = nextResult - - override fun visitSourceFile(node: SyntaxNode): R = visitChildren(node) - override fun visitPackageHeader(node: SyntaxNode): R = visitChildren(node) - override fun visitImport(node: SyntaxNode): R = visitChildren(node) - override fun visitClassDeclaration(node: SyntaxNode): R = visitChildren(node) - override fun visitObjectDeclaration(node: SyntaxNode): R = visitChildren(node) - override fun visitFunctionDeclaration(node: SyntaxNode): R = visitChildren(node) - override fun visitPropertyDeclaration(node: SyntaxNode): R = visitChildren(node) - override fun visitTypeAlias(node: SyntaxNode): R = visitChildren(node) - override fun visitParameter(node: SyntaxNode): R = visitChildren(node) - override fun visitTypeParameter(node: SyntaxNode): R = visitChildren(node) - override fun visitType(node: SyntaxNode): R = visitChildren(node) - override fun visitBlock(node: SyntaxNode): R = visitChildren(node) - override fun visitIfExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitWhenExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitTryExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitLoop(node: SyntaxNode): R = visitChildren(node) - override fun visitCallExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitNavigationExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitIndexingExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitBinaryExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitUnaryExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitLambdaExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitObjectLiteral(node: SyntaxNode): R = visitChildren(node) - override fun visitThisOrSuperExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitJumpExpression(node: SyntaxNode): R = visitChildren(node) - override fun visitLiteral(node: SyntaxNode): R = defaultResult() - override fun visitIdentifier(node: SyntaxNode): R = defaultResult() - override fun visitStringTemplate(node: SyntaxNode): R = visitChildren(node) - override fun visitAnnotation(node: SyntaxNode): R = visitChildren(node) - override fun visitModifier(node: SyntaxNode): R = defaultResult() - override fun visitOther(node: SyntaxNode): R = visitChildren(node) -} - -/** - * A visitor that collects nodes matching a predicate. - * - * Useful for finding all nodes of a specific type: - * - * ```kotlin - * val collector = NodeCollector { it.kind == SyntaxKind.FUNCTION_DECLARATION } - * val functions = collector.visit(tree.root) - * ``` - */ -class NodeCollector( - private val predicate: (SyntaxNode) -> Boolean -) : SyntaxVisitorBase>() { - - private val collected = mutableListOf() - - override fun defaultResult(): List = collected - - override fun visit(node: SyntaxNode): List { - if (predicate(node)) { - collected.add(node) - } - visitChildren(node) - return collected - } - - override fun aggregateResult(aggregate: List, nextResult: List): List { - return aggregate - } -} - -/** - * A visitor that counts nodes matching a predicate. - * - * ```kotlin - * val counter = NodeCounter { SyntaxKind.isExpression(it.kind) } - * val expressionCount = counter.visit(tree.root) - * ``` - */ -class NodeCounter( - private val predicate: (SyntaxNode) -> Boolean -) : SyntaxVisitorBase() { - - private var count = 0 - - override fun defaultResult(): Int = count - - override fun visit(node: SyntaxNode): Int { - if (predicate(node)) { - count++ - } - visitChildren(node) - return count - } - - override fun aggregateResult(aggregate: Int, nextResult: Int): Int = aggregate -} - -/** - * A simple visitor that performs an action on each node without returning a value. - * - * ```kotlin - * val printer = NodeWalker { node -> - * println("${node.kind} at ${node.range}") - * } - * printer.visit(tree.root) - * ``` - */ -class NodeWalker( - private val action: (SyntaxNode) -> Unit -) : SyntaxVisitorBase() { - - override fun defaultResult() = Unit - - override fun visit(node: SyntaxNode) { - action(node) - visitChildren(node) - } - - override fun aggregateResult(aggregate: Unit, nextResult: Unit) = Unit -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/TextRange.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/TextRange.kt deleted file mode 100644 index f8d9d9f836..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/parser/TextRange.kt +++ /dev/null @@ -1,253 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.parser - -/** - * Represents a range of text in source code, defined by start and end positions. - * - * ## Range Semantics - * - * Ranges are **inclusive** at the start and **exclusive** at the end (half-open interval). - * This matches standard programming conventions and LSP specification. - * - * ``` - * Source: fun main() - * Range: ^^^ - * start: (0, 0) - * end: (0, 3) -- points AFTER 'n', so "fun" has length 3 - * ``` - * - * ## Byte vs Character Positions - * - * Column numbers represent **byte offsets**, not character offsets. For ASCII text, - * these are identical. For UTF-8 text with multi-byte characters, byte offsets may - * be larger than character counts. - * - * ## Immutability - * - * TextRange instances are immutable. All operations return new instances. - * - * ## Usage - * - * ```kotlin - * val range = TextRange( - * start = Position(0, 4), - * end = Position(0, 8) - * ) - * println(range.contains(Position(0, 5))) // true - * ``` - * - * @property start The starting position (inclusive) - * @property end The ending position (exclusive) - */ -data class TextRange( - val start: Position, - val end: Position, - val startOffset: Int = -1, - val endOffset: Int = -1 -) { - init { - require(start <= end) { - "Start position ($start) must not be after end position ($end)" - } - } - - val hasOffsets: Boolean get() = startOffset >= 0 && endOffset >= 0 - - /** - * Zero-based start byte offset. - * This is derived from the position; for actual byte offset, use [startByte]. - */ - val startLine: Int get() = start.line - - /** Zero-based end line */ - val endLine: Int get() = end.line - - /** Zero-based start column (byte offset within line) */ - val startColumn: Int get() = start.column - - /** Zero-based end column (byte offset within line) */ - val endColumn: Int get() = end.column - - /** - * Number of lines this range spans. - * - * A single-line range returns 1. - * A range from line 0 to line 2 returns 3. - */ - val lineCount: Int get() = endLine - startLine + 1 - - /** - * Whether this range spans multiple lines. - */ - val isMultiLine: Boolean get() = startLine != endLine - - /** - * Whether this range is empty (start equals end). - * - * Empty ranges represent cursor positions or zero-width locations. - */ - val isEmpty: Boolean get() = start == end - - /** - * Approximate length of this range in terms of position. - * - * For single-line ranges, this is the column difference. - * For multi-line ranges, this is a rough approximation. - */ - val length: Int get() = if (isMultiLine) { - (endLine - startLine) * 80 + endColumn - } else { - endColumn - startColumn - } - - /** - * Checks if the given position falls within this range. - * - * A position is contained if it is >= start and < end (half-open). - * - * @param position The position to check - * @return true if position is within the range - */ - operator fun contains(position: Position): Boolean { - return position >= start && position < end - } - - /** - * Checks if another range is completely contained within this range. - * - * @param other The range to check - * @return true if other is fully within this range - */ - operator fun contains(other: TextRange): Boolean { - return other.start >= start && other.end <= end - } - - /** - * Checks if this range overlaps with another range. - * - * Two ranges overlap if they share at least one position. - * Adjacent ranges (one ends where other starts) do NOT overlap. - * - * @param other The range to check - * @return true if ranges overlap - */ - fun overlaps(other: TextRange): Boolean { - return start < other.end && end > other.start - } - - /** - * Checks if this range is adjacent to another range. - * - * Ranges are adjacent if one ends exactly where the other begins. - * - * @param other The range to check - * @return true if ranges are adjacent - */ - fun adjacentTo(other: TextRange): Boolean { - return end == other.start || start == other.end - } - - /** - * Returns the intersection of this range with another, or null if they don't overlap. - * - * @param other The range to intersect with - * @return The overlapping portion, or null if no overlap - */ - fun intersect(other: TextRange): TextRange? { - val newStart = maxOf(start, other.start) - val newEnd = minOf(end, other.end) - return if (newStart < newEnd) TextRange(newStart, newEnd) else null - } - - /** - * Returns the smallest range that contains both this range and another. - * - * @param other The range to merge with - * @return A range spanning both ranges - */ - fun union(other: TextRange): TextRange { - return TextRange( - start = minOf(start, other.start), - end = maxOf(end, other.end) - ) - } - - /** - * Extends this range to include the given position. - * - * @param position The position to include - * @return A range that includes both the original range and the position - */ - fun extendTo(position: Position): TextRange { - return TextRange( - start = minOf(start, position), - end = maxOf(end, position) - ) - } - - /** - * Creates a new range with the start position offset. - * - * @param lineDelta Lines to add to start - * @param columnDelta Columns to add to start - * @return New range with offset start position - */ - fun offsetStart(lineDelta: Int, columnDelta: Int): TextRange { - return copy(start = start.offset(lineDelta, columnDelta)) - } - - /** - * Creates a new range with the end position offset. - * - * @param lineDelta Lines to add to end - * @param columnDelta Columns to add to end - * @return New range with offset end position - */ - fun offsetEnd(lineDelta: Int, columnDelta: Int): TextRange { - return copy(end = end.offset(lineDelta, columnDelta)) - } - - /** - * Returns a human-readable string representation. - * Uses one-based line/column numbers for display. - */ - override fun toString(): String { - return if (isMultiLine || start != end) { - "$start-$end" - } else { - start.toString() - } - } - - companion object { - /** An empty range at the start of a document */ - val ZERO: TextRange = TextRange(Position.ZERO, Position.ZERO) - - /** Alias for ZERO - an empty range */ - val EMPTY: TextRange = ZERO - - /** - * Creates a single-line range from column indices. - * - * @param line Zero-based line number - * @param startColumn Zero-based start column - * @param endColumn Zero-based end column - * @return Range spanning the columns on the given line - */ - fun onLine(line: Int, startColumn: Int, endColumn: Int): TextRange { - return TextRange( - start = Position(line, startColumn), - end = Position(line, endColumn) - ) - } - - /** - * Creates a range representing a single position (zero-width). - * - * @param position The position - * @return An empty range at the given position - */ - fun at(position: Position): TextRange { - return TextRange(position, position) - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/AnalysisContext.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/AnalysisContext.kt deleted file mode 100644 index 8f99314412..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/AnalysisContext.kt +++ /dev/null @@ -1,264 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.semantic - -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.index.StdlibIndex -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxKind -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxNode -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxTree -import org.appdevforall.codeonthego.lsp.kotlin.parser.TextRange -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Scope -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeReference -import org.appdevforall.codeonthego.lsp.kotlin.types.ImportAwareTypeResolver -import org.appdevforall.codeonthego.lsp.kotlin.types.KotlinType -import org.appdevforall.codeonthego.lsp.kotlin.types.TypeChecker -import org.appdevforall.codeonthego.lsp.kotlin.types.TypeHierarchy - -/** - * Shared context for semantic analysis of a file. - * - * AnalysisContext holds all the state needed during semantic analysis: - * - The syntax tree being analyzed - * - The symbol table with all declarations - * - Type information for expressions - * - Diagnostics collector for errors/warnings - * - Type checker and resolver instances - * - * ## Usage - * - * ```kotlin - * val context = AnalysisContext(tree, symbolTable, filePath) - * val inferrer = TypeInferrer(context) - * val type = inferrer.inferType(expression) - * ``` - * - * @property tree The parsed syntax tree - * @property symbolTable The symbol table for this file - * @property filePath Path to the source file - */ -class AnalysisContext( - val tree: SyntaxTree, - val symbolTable: SymbolTable, - val filePath: String, - val stdlibIndex: StdlibIndex? = null, - val projectIndex: ProjectIndex? = null, - private val syntaxErrorRanges: List = emptyList() -) { - val diagnostics: DiagnosticCollector = DiagnosticCollector() - - val typeChecker: TypeChecker = TypeChecker() - - val typeResolver: ImportAwareTypeResolver = ImportAwareTypeResolver(symbolTable, projectIndex) - - val typeHierarchy: TypeHierarchy = TypeHierarchy.DEFAULT - - fun resolveType(ref: TypeReference, scope: Scope? = null): KotlinType { - return typeResolver.resolve(ref, scope) - } - - private val expressionTypes = mutableMapOf() - - private val resolvedReferences = mutableMapOf() - - private val smartCasts = mutableMapOf() - - private val symbolTypes = mutableMapOf() - - private val activeSmartCasts = mutableMapOf>() - - private val scopedSmartCasts = mutableListOf() - - private val computingTypes = mutableSetOf() - - val fileScope: Scope get() = symbolTable.fileScope - - val packageName: String get() = symbolTable.packageName - - val hasErrors: Boolean get() = diagnostics.hasErrors - - fun recordType(node: SyntaxNode, type: KotlinType) { - expressionTypes[node] = type - } - - fun getType(node: SyntaxNode): KotlinType? { - return expressionTypes[node] - } - - fun recordReference(node: SyntaxNode, symbol: Symbol) { - resolvedReferences[node] = symbol - } - - fun getResolvedSymbol(node: SyntaxNode): Symbol? { - return resolvedReferences[node] - } - - fun recordSmartCast(node: SyntaxNode, info: SmartCastInfo) { - smartCasts[node] = info - } - - fun getSmartCast(node: SyntaxNode): SmartCastInfo? { - return smartCasts[node] - } - - fun pushSmartCast(symbol: Symbol, info: SmartCastInfo, scopeNode: SyntaxNode? = null) { - activeSmartCasts.getOrPut(symbol) { mutableListOf() }.add(info) - if (scopeNode != null) { - scopedSmartCasts.add(ScopedSmartCast(symbol, info, scopeNode.range)) - } - } - - fun popSmartCast(symbol: Symbol) { - activeSmartCasts[symbol]?.removeLastOrNull() - if (activeSmartCasts[symbol]?.isEmpty() == true) { - activeSmartCasts.remove(symbol) - } - } - - fun getActiveSmartCast(symbol: Symbol): SmartCastInfo? { - return activeSmartCasts[symbol]?.lastOrNull() - } - - fun getSmartCastType(symbol: Symbol): KotlinType? { - return getActiveSmartCast(symbol)?.castType - } - - fun getSmartCastTypeAtPosition(symbol: Symbol, line: Int, column: Int): KotlinType? { - return scopedSmartCasts - .filter { it.symbol == symbol && it.scopeRange.containsPosition(line, column) } - .maxByOrNull { it.scopeRange.start.line * 10000 + it.scopeRange.start.column } - ?.info?.castType - } - - fun recordSymbolType(symbol: Symbol, type: KotlinType) { - symbolTypes[symbol] = type - } - - fun getSymbolType(symbol: Symbol): KotlinType? { - return symbolTypes[symbol] - } - - fun startComputingType(symbol: Symbol): Boolean { - return computingTypes.add(symbol) - } - - fun finishComputingType(symbol: Symbol) { - computingTypes.remove(symbol) - } - - fun isComputingType(symbol: Symbol): Boolean { - return symbol in computingTypes - } - - fun reportError(code: DiagnosticCode, node: SyntaxNode, vararg args: Any) { - if (isInsideSyntaxErrorRegion(node)) { - return - } - diagnostics.error(code, node.range, *args, filePath = filePath) - } - - private fun isInsideSyntaxErrorRegion(node: SyntaxNode): Boolean { - if (syntaxErrorRanges.isNotEmpty()) { - val nodeRange = node.range - for (errorRange in syntaxErrorRanges) { - if (errorRange.contains(nodeRange)) { - return true - } - } - } - - var current: SyntaxNode? = node - while (current != null) { - if (current.isError || current.kind == SyntaxKind.ERROR) { - return true - } - if (isStatementLevelNode(current.kind)) { - return current.hasError - } - if (isBlockBoundary(current.kind)) { - return false - } - current = current.parent - } - return false - } - - private fun isStatementLevelNode(kind: SyntaxKind): Boolean { - return kind in setOf( - SyntaxKind.CALL_EXPRESSION, - SyntaxKind.PROPERTY_DECLARATION, - SyntaxKind.ASSIGNMENT, - SyntaxKind.AUGMENTED_ASSIGNMENT, - SyntaxKind.RETURN, - SyntaxKind.IF_EXPRESSION, - SyntaxKind.WHEN_EXPRESSION, - SyntaxKind.FOR_STATEMENT, - SyntaxKind.WHILE_STATEMENT, - SyntaxKind.DO_WHILE_STATEMENT, - SyntaxKind.TRY_EXPRESSION, - SyntaxKind.NAVIGATION_EXPRESSION - ) - } - - private fun isBlockBoundary(kind: SyntaxKind): Boolean { - return kind in setOf( - SyntaxKind.FUNCTION_BODY, - SyntaxKind.CLASS_BODY, - SyntaxKind.FUNCTION_DECLARATION, - SyntaxKind.CLASS_DECLARATION, - SyntaxKind.OBJECT_DECLARATION, - SyntaxKind.SOURCE_FILE - ) - } - - fun reportWarning(code: DiagnosticCode, node: SyntaxNode, vararg args: Any) { - diagnostics.warning(code, node.range, *args, filePath = filePath) - } - - fun createChildContext(): AnalysisContext { - return AnalysisContext(tree, symbolTable, filePath, stdlibIndex, projectIndex, syntaxErrorRanges).also { - it.diagnostics.merge(diagnostics) - } - } -} - -/** - * Information about a smart cast. - */ -data class SmartCastInfo( - val originalType: KotlinType, - val castType: KotlinType, - val condition: SyntaxNode -) - -data class ScopedSmartCast( - val symbol: Symbol, - val info: SmartCastInfo, - val scopeRange: TextRange -) - -private fun TextRange.containsPosition(line: Int, column: Int): Boolean { - if (line < start.line || line > end.line) return false - if (line == start.line && column < start.column) return false - if (line == end.line && column > end.column) return false - return true -} - -/** - * Result of semantic analysis for a file. - */ -data class AnalysisResult( - val symbolTable: SymbolTable, - val diagnostics: List, - val analysisTimeMs: Long -) { - val hasErrors: Boolean get() = diagnostics.any { it.isError } - - val errors: List get() = diagnostics.filter { it.isError } - - val warnings: List get() = diagnostics.filter { it.isWarning } - - val errorCount: Int get() = diagnostics.count { it.isError } - - val warningCount: Int get() = diagnostics.count { it.isWarning } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/Diagnostic.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/Diagnostic.kt deleted file mode 100644 index 250a4ff914..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/Diagnostic.kt +++ /dev/null @@ -1,489 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.semantic - -import org.appdevforall.codeonthego.lsp.kotlin.parser.TextRange - -/** - * Severity level for diagnostics. - */ -enum class DiagnosticSeverity { - ERROR, - WARNING, - INFO, - HINT; - - val isError: Boolean get() = this == ERROR - val isWarning: Boolean get() = this == WARNING -} - -/** - * Diagnostic codes for all possible errors and warnings. - * - * Each code has a unique identifier and default severity. - */ -enum class DiagnosticCode( - val id: String, - val defaultSeverity: DiagnosticSeverity, - val messageTemplate: String -) { - UNRESOLVED_REFERENCE( - "UNRESOLVED_REFERENCE", - DiagnosticSeverity.ERROR, - "Unresolved reference: {0}" - ), - - TYPE_MISMATCH( - "TYPE_MISMATCH", - DiagnosticSeverity.ERROR, - "Type mismatch: expected {0}, found {1}" - ), - - UNRESOLVED_TYPE( - "UNRESOLVED_TYPE", - DiagnosticSeverity.ERROR, - "Unresolved type: {0}" - ), - - WRONG_NUMBER_OF_ARGUMENTS( - "WRONG_NUMBER_OF_ARGUMENTS", - DiagnosticSeverity.ERROR, - "Wrong number of arguments: expected {0}, found {1}" - ), - - NO_VALUE_PASSED_FOR_PARAMETER( - "NO_VALUE_PASSED_FOR_PARAMETER", - DiagnosticSeverity.ERROR, - "No value passed for parameter '{0}'" - ), - - ARGUMENT_TYPE_MISMATCH( - "ARGUMENT_TYPE_MISMATCH", - DiagnosticSeverity.ERROR, - "Argument type mismatch: expected {0}, found {1}" - ), - - NONE_APPLICABLE( - "NONE_APPLICABLE", - DiagnosticSeverity.ERROR, - "None of the following functions can be called with the arguments supplied: {0}" - ), - - OVERLOAD_RESOLUTION_AMBIGUITY( - "OVERLOAD_RESOLUTION_AMBIGUITY", - DiagnosticSeverity.ERROR, - "Overload resolution ambiguity between: {0}" - ), - - RETURN_TYPE_MISMATCH( - "RETURN_TYPE_MISMATCH", - DiagnosticSeverity.ERROR, - "Return type mismatch: expected {0}, found {1}" - ), - - RETURN_NOT_ALLOWED( - "RETURN_NOT_ALLOWED", - DiagnosticSeverity.ERROR, - "'return' is not allowed here" - ), - - UNREACHABLE_CODE( - "UNREACHABLE_CODE", - DiagnosticSeverity.WARNING, - "Unreachable code" - ), - - VARIABLE_WITH_NO_TYPE_NO_INITIALIZER( - "VARIABLE_WITH_NO_TYPE_NO_INITIALIZER", - DiagnosticSeverity.ERROR, - "This variable must either have a type annotation or be initialized" - ), - - VAL_REASSIGNMENT( - "VAL_REASSIGNMENT", - DiagnosticSeverity.ERROR, - "Val cannot be reassigned" - ), - - VAR_INITIALIZATION_REQUIRED( - "VAR_INITIALIZATION_REQUIRED", - DiagnosticSeverity.ERROR, - "Property must be initialized or be abstract" - ), - - UNINITIALIZED_VARIABLE( - "UNINITIALIZED_VARIABLE", - DiagnosticSeverity.ERROR, - "Variable '{0}' must be initialized" - ), - - NULLABLE_TYPE_MISMATCH( - "NULLABLE_TYPE_MISMATCH", - DiagnosticSeverity.ERROR, - "Type mismatch: inferred type is {0} but {1} was expected" - ), - - UNSAFE_CALL( - "UNSAFE_CALL", - DiagnosticSeverity.ERROR, - "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type {0}" - ), - - UNSAFE_IMPLICIT_INVOKE_CALL( - "UNSAFE_IMPLICIT_INVOKE_CALL", - DiagnosticSeverity.ERROR, - "Reference has a nullable type '{0}', use explicit ?.invoke() to make a function-like call" - ), - - USELESS_NULLABLE_CHECK( - "USELESS_NULLABLE_CHECK", - DiagnosticSeverity.WARNING, - "Unnecessary safe call on a non-null receiver of type {0}" - ), - - SENSELESS_NULL_IN_WHEN( - "SENSELESS_NULL_IN_WHEN", - DiagnosticSeverity.WARNING, - "Null can not be a value of a non-null type {0}" - ), - - ABSTRACT_MEMBER_NOT_IMPLEMENTED( - "ABSTRACT_MEMBER_NOT_IMPLEMENTED", - DiagnosticSeverity.ERROR, - "Class '{0}' is not abstract and does not implement abstract member '{1}'" - ), - - ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED( - "ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED", - DiagnosticSeverity.ERROR, - "Class '{0}' must be declared abstract or implement abstract member '{1}'" - ), - - CANNOT_OVERRIDE_INVISIBLE_MEMBER( - "CANNOT_OVERRIDE_INVISIBLE_MEMBER", - DiagnosticSeverity.ERROR, - "Cannot override invisible member" - ), - - NOTHING_TO_OVERRIDE( - "NOTHING_TO_OVERRIDE", - DiagnosticSeverity.ERROR, - "'{0}' overrides nothing" - ), - - MUST_BE_INITIALIZED_OR_BE_ABSTRACT( - "MUST_BE_INITIALIZED_OR_BE_ABSTRACT", - DiagnosticSeverity.ERROR, - "Property '{0}' must be initialized or be abstract" - ), - - CONFLICTING_OVERLOADS( - "CONFLICTING_OVERLOADS", - DiagnosticSeverity.ERROR, - "Conflicting overloads: {0}" - ), - - REDECLARATION( - "REDECLARATION", - DiagnosticSeverity.ERROR, - "Conflicting declarations: {0}" - ), - - NAME_SHADOWING( - "NAME_SHADOWING", - DiagnosticSeverity.WARNING, - "Name '{0}' shadows a declaration from outer scope" - ), - - UNUSED_VARIABLE( - "UNUSED_VARIABLE", - DiagnosticSeverity.WARNING, - "Variable '{0}' is never used" - ), - - UNUSED_PARAMETER( - "UNUSED_PARAMETER", - DiagnosticSeverity.WARNING, - "Parameter '{0}' is never used" - ), - - UNUSED_EXPRESSION( - "UNUSED_EXPRESSION", - DiagnosticSeverity.WARNING, - "The expression is unused" - ), - - USELESS_CAST( - "USELESS_CAST", - DiagnosticSeverity.WARNING, - "No cast needed" - ), - - UNCHECKED_CAST( - "UNCHECKED_CAST", - DiagnosticSeverity.WARNING, - "Unchecked cast: {0} to {1}" - ), - - DEPRECATION( - "DEPRECATION", - DiagnosticSeverity.WARNING, - "'{0}' is deprecated. {1}" - ), - - MISSING_WHEN_BRANCH( - "MISSING_WHEN_BRANCH", - DiagnosticSeverity.ERROR, - "'when' expression must be exhaustive, add necessary {0} branches" - ), - - UNREACHABLE_WHEN_BRANCH( - "UNREACHABLE_WHEN_BRANCH", - DiagnosticSeverity.WARNING, - "This branch is unreachable because of previous branches" - ), - - CONDITION_TYPE_MISMATCH( - "CONDITION_TYPE_MISMATCH", - DiagnosticSeverity.ERROR, - "Condition must be of type Boolean, but is {0}" - ), - - INCOMPATIBLE_TYPES( - "INCOMPATIBLE_TYPES", - DiagnosticSeverity.ERROR, - "Incompatible types: {0} and {1}" - ), - - OPERATOR_MODIFIER_REQUIRED( - "OPERATOR_MODIFIER_REQUIRED", - DiagnosticSeverity.ERROR, - "'operator' modifier is required on '{0}'" - ), - - INFIX_MODIFIER_REQUIRED( - "INFIX_MODIFIER_REQUIRED", - DiagnosticSeverity.ERROR, - "'infix' modifier is required on '{0}'" - ), - - EXTENSION_FUNCTION_SHADOWED_BY_MEMBER( - "EXTENSION_FUNCTION_SHADOWED_BY_MEMBER", - DiagnosticSeverity.WARNING, - "Extension function '{0}' is shadowed by a member" - ), - - TOO_MANY_ARGUMENTS( - "TOO_MANY_ARGUMENTS", - DiagnosticSeverity.ERROR, - "Too many arguments for {0}" - ), - - NAMED_PARAMETER_NOT_FOUND( - "NAMED_PARAMETER_NOT_FOUND", - DiagnosticSeverity.ERROR, - "Cannot find a parameter with this name: {0}" - ), - - MIXING_NAMED_AND_POSITIONED_ARGUMENTS( - "MIXING_NAMED_AND_POSITIONED_ARGUMENTS", - DiagnosticSeverity.ERROR, - "Mixing named and positioned arguments is not allowed" - ), - - SUPER_NOT_AVAILABLE( - "SUPER_NOT_AVAILABLE", - DiagnosticSeverity.ERROR, - "'super' is not an expression" - ), - - THIS_NOT_AVAILABLE( - "THIS_NOT_AVAILABLE", - DiagnosticSeverity.ERROR, - "'this' is not available" - ), - - SYNTAX_ERROR( - "SYNTAX_ERROR", - DiagnosticSeverity.ERROR, - "Syntax error: {0}" - ); - - fun format(vararg args: Any): String { - var message = messageTemplate - args.forEachIndexed { index, arg -> - message = message.replace("{$index}", arg.toString()) - } - return message - } -} - -/** - * A diagnostic message (error, warning, info, or hint). - * - * Diagnostics are produced during semantic analysis and represent - * problems or suggestions in the code. - * - * @property code The diagnostic code identifying the issue type - * @property message Human-readable description of the issue - * @property severity The severity level - * @property range The source location where the issue occurs - * @property filePath Path to the file containing the issue - * @property relatedInfo Additional related locations/information - */ -data class Diagnostic( - val code: DiagnosticCode, - val message: String, - val severity: DiagnosticSeverity, - val range: TextRange, - val filePath: String = "", - val relatedInfo: List = emptyList() -) { - val isError: Boolean get() = severity.isError - val isWarning: Boolean get() = severity.isWarning - - override fun toString(): String = buildString { - append(severity.name.lowercase()) - append(": ") - append(message) - append(" [") - append(code.id) - append("]") - if (filePath.isNotEmpty()) { - append(" at ") - append(filePath) - append(":") - append(range.start.displayLine) - append(":") - append(range.start.displayColumn) - } - } - - companion object { - fun error( - code: DiagnosticCode, - range: TextRange, - vararg args: Any, - filePath: String = "" - ): Diagnostic = Diagnostic( - code = code, - message = code.format(*args), - severity = DiagnosticSeverity.ERROR, - range = range, - filePath = filePath - ) - - fun warning( - code: DiagnosticCode, - range: TextRange, - vararg args: Any, - filePath: String = "" - ): Diagnostic = Diagnostic( - code = code, - message = code.format(*args), - severity = DiagnosticSeverity.WARNING, - range = range, - filePath = filePath - ) - - fun info( - code: DiagnosticCode, - range: TextRange, - vararg args: Any, - filePath: String = "" - ): Diagnostic = Diagnostic( - code = code, - message = code.format(*args), - severity = DiagnosticSeverity.INFO, - range = range, - filePath = filePath - ) - - fun hint( - code: DiagnosticCode, - range: TextRange, - vararg args: Any, - filePath: String = "" - ): Diagnostic = Diagnostic( - code = code, - message = code.format(*args), - severity = DiagnosticSeverity.HINT, - range = range, - filePath = filePath - ) - } -} - -/** - * Related information for a diagnostic. - */ -data class DiagnosticRelatedInfo( - val message: String, - val range: TextRange, - val filePath: String -) - -/** - * Collector for accumulating diagnostics during analysis. - */ -class DiagnosticCollector { - private val _diagnostics = mutableListOf() - - val diagnostics: List get() = _diagnostics.toList() - - val errors: List get() = _diagnostics.filter { it.isError } - - val warnings: List get() = _diagnostics.filter { it.isWarning } - - val hasErrors: Boolean get() = _diagnostics.any { it.isError } - - val errorCount: Int get() = _diagnostics.count { it.isError } - - val warningCount: Int get() = _diagnostics.count { it.isWarning } - - fun report(diagnostic: Diagnostic) { - _diagnostics.add(diagnostic) - } - - fun error(code: DiagnosticCode, range: TextRange, vararg args: Any, filePath: String = "") { - report(Diagnostic.error(code, range, *args, filePath = filePath)) - } - - fun warning(code: DiagnosticCode, range: TextRange, vararg args: Any, filePath: String = "") { - report(Diagnostic.warning(code, range, *args, filePath = filePath)) - } - - fun info(code: DiagnosticCode, range: TextRange, vararg args: Any, filePath: String = "") { - report(Diagnostic.info(code, range, *args, filePath = filePath)) - } - - fun hint(code: DiagnosticCode, range: TextRange, vararg args: Any, filePath: String = "") { - report(Diagnostic.hint(code, range, *args, filePath = filePath)) - } - - fun unresolvedReference(name: String, range: TextRange, filePath: String = "") { - error(DiagnosticCode.UNRESOLVED_REFERENCE, range, name, filePath = filePath) - } - - fun typeMismatch(expected: String, actual: String, range: TextRange, filePath: String = "") { - error(DiagnosticCode.TYPE_MISMATCH, range, expected, actual, filePath = filePath) - } - - fun unresolvedType(name: String, range: TextRange, filePath: String = "") { - error(DiagnosticCode.UNRESOLVED_TYPE, range, name, filePath = filePath) - } - - fun clear() { - _diagnostics.clear() - } - - fun merge(other: DiagnosticCollector) { - _diagnostics.addAll(other._diagnostics) - } - - fun diagnosticsInRange(range: TextRange): List { - return _diagnostics.filter { it.range.overlaps(range) } - } - - fun diagnosticsAtLine(line: Int): List { - return _diagnostics.filter { it.range.startLine == line || it.range.endLine == line } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/OverloadResolver.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/OverloadResolver.kt deleted file mode 100644 index 0cc664530e..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/OverloadResolver.kt +++ /dev/null @@ -1,495 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.semantic - -import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Scope -import org.appdevforall.codeonthego.lsp.kotlin.types.ClassType -import org.appdevforall.codeonthego.lsp.kotlin.types.KotlinType -import org.appdevforall.codeonthego.lsp.kotlin.types.PrimitiveType -import org.appdevforall.codeonthego.lsp.kotlin.types.TypeChecker - -/** - * Resolves function overloads to select the most specific applicable candidate. - * - * OverloadResolver implements Kotlin's overload resolution algorithm: - * 1. Filter to applicable candidates - * 2. Rank by specificity - * 3. Select the most specific candidate - * - * ## Applicability - * - * A function is applicable if: - * - Number of arguments matches parameters (considering defaults and varargs) - * - Each argument type is assignable to the parameter type - * - Named arguments match parameter names - * - * ## Specificity - * - * Given two applicable candidates, one is more specific if: - * - Its parameter types are subtypes of the other's parameter types - * - It has fewer default parameters used - * - It's a member (not extension) when both apply - * - * ## Usage - * - * ```kotlin - * val resolver = OverloadResolver(context) - * val result = resolver.resolve(candidates, arguments) - * when (result) { - * is OverloadResolutionResult.Success -> result.selected - * is OverloadResolutionResult.Ambiguity -> // handle multiple candidates - * is OverloadResolutionResult.NoApplicable -> // handle no match - * } - * ``` - */ -class OverloadResolver( - private val context: AnalysisContext -) { - private val typeChecker: TypeChecker = context.typeChecker - - /** - * Result of overload resolution. - */ - sealed class OverloadResolutionResult { - /** - * Successfully resolved to a single function. - */ - data class Success( - val selected: FunctionSymbol, - val parameterMapping: ParameterMapping - ) : OverloadResolutionResult() - - /** - * Multiple candidates are equally specific. - */ - data class Ambiguity( - val candidates: List - ) : OverloadResolutionResult() - - /** - * No candidates are applicable. - */ - data class NoApplicable( - val candidates: List, - val diagnostics: List - ) : OverloadResolutionResult() - - val isSuccess: Boolean get() = this is Success - } - - /** - * Mapping of arguments to parameters after resolution. - */ - data class ParameterMapping( - val positionalMappings: Map, - val namedMappings: Map, - val defaultsUsed: Set, - val varargElements: List - ) - - /** - * Diagnostic for why a candidate is not applicable. - */ - data class ApplicabilityDiagnostic( - val candidate: FunctionSymbol, - val reason: ApplicabilityFailure - ) - - /** - * Reason for applicability failure. - */ - sealed class ApplicabilityFailure { - data class TooFewArguments(val required: Int, val provided: Int) : ApplicabilityFailure() - data class TooManyArguments(val max: Int, val provided: Int) : ApplicabilityFailure() - data class TypeMismatch(val paramIndex: Int, val expected: KotlinType, val actual: KotlinType) : ApplicabilityFailure() - data class NamedParameterNotFound(val name: String) : ApplicabilityFailure() - data class DuplicateNamedArgument(val name: String) : ApplicabilityFailure() - data class PositionalAfterNamed(val argIndex: Int) : ApplicabilityFailure() - } - - /** - * Argument in a function call. - */ - data class CallArgument( - val type: KotlinType, - val name: String? = null, - val isSpread: Boolean = false - ) - - /** - * Resolves overloads for a function call. - * - * @param candidates List of candidate functions with the same name - * @param arguments The call arguments with types - * @param expectedReturnType Optional expected return type for disambiguation - * @param scope The current scope - * @return Resolution result - */ - fun resolve( - candidates: List, - arguments: List, - expectedReturnType: KotlinType? = null, - scope: Scope? = null - ): OverloadResolutionResult { - if (candidates.isEmpty()) { - return OverloadResolutionResult.NoApplicable(emptyList(), emptyList()) - } - - if (candidates.size == 1) { - val candidate = candidates.first() - val applicability = checkApplicability(candidate, arguments, scope) - return when (applicability) { - is ApplicabilityResult.Applicable -> { - OverloadResolutionResult.Success(candidate, applicability.mapping) - } - is ApplicabilityResult.NotApplicable -> { - OverloadResolutionResult.NoApplicable( - candidates, - listOf(ApplicabilityDiagnostic(candidate, applicability.reason)) - ) - } - } - } - - val applicableCandidates = mutableListOf>() - val diagnostics = mutableListOf() - - for (candidate in candidates) { - val applicability = checkApplicability(candidate, arguments, scope) - when (applicability) { - is ApplicabilityResult.Applicable -> { - applicableCandidates.add(candidate to applicability.mapping) - } - is ApplicabilityResult.NotApplicable -> { - diagnostics.add(ApplicabilityDiagnostic(candidate, applicability.reason)) - } - } - } - - if (applicableCandidates.isEmpty()) { - return OverloadResolutionResult.NoApplicable(candidates, diagnostics) - } - - if (applicableCandidates.size == 1) { - val (selected, mapping) = applicableCandidates.first() - return OverloadResolutionResult.Success(selected, mapping) - } - - val ranked = rankCandidates(applicableCandidates, arguments, expectedReturnType, scope) - - return when { - ranked.size == 1 -> { - val (selected, mapping) = ranked.first() - OverloadResolutionResult.Success(selected, mapping) - } - ranked.all { (f, _) -> isMoreSpecific(ranked.first().first, f, arguments, scope) } -> { - val (selected, mapping) = ranked.first() - OverloadResolutionResult.Success(selected, mapping) - } - else -> { - OverloadResolutionResult.Ambiguity(ranked.map { it.first }) - } - } - } - - private sealed class ApplicabilityResult { - data class Applicable(val mapping: ParameterMapping) : ApplicabilityResult() - data class NotApplicable(val reason: ApplicabilityFailure) : ApplicabilityResult() - } - - private fun checkApplicability( - function: FunctionSymbol, - arguments: List, - scope: Scope? - ): ApplicabilityResult { - val parameters = function.parameters - val positionalMappings = mutableMapOf() - val namedMappings = mutableMapOf() - val usedParams = mutableSetOf() - val varargElements = mutableListOf() - - var positionalIndex = 0 - var seenNamed = false - - for ((argIndex, argument) in arguments.withIndex()) { - if (argument.name != null) { - seenNamed = true - val paramIndex = parameters.indexOfFirst { it.name == argument.name } - if (paramIndex == -1) { - return ApplicabilityResult.NotApplicable( - ApplicabilityFailure.NamedParameterNotFound(argument.name) - ) - } - if (paramIndex in usedParams) { - return ApplicabilityResult.NotApplicable( - ApplicabilityFailure.DuplicateNamedArgument(argument.name) - ) - } - - val param = parameters[paramIndex] - val paramType = resolveParamType(param, function, scope) - if (!isArgumentCompatible(argument, paramType)) { - return ApplicabilityResult.NotApplicable( - ApplicabilityFailure.TypeMismatch(paramIndex, paramType, argument.type) - ) - } - - namedMappings[argument.name] = argIndex - usedParams.add(paramIndex) - } else { - if (seenNamed) { - return ApplicabilityResult.NotApplicable( - ApplicabilityFailure.PositionalAfterNamed(argIndex) - ) - } - - while (positionalIndex in usedParams) { - positionalIndex++ - } - - val param = parameters.getOrNull(positionalIndex) - - if (param == null) { - val varargParam = parameters.find { it.isVararg } - if (varargParam != null) { - val varargIndex = parameters.indexOf(varargParam) - val elementType = resolveVarargElementType(varargParam, function, scope) - if (!isArgumentCompatible(argument, elementType)) { - return ApplicabilityResult.NotApplicable( - ApplicabilityFailure.TypeMismatch(varargIndex, elementType, argument.type) - ) - } - varargElements.add(argIndex) - continue - } - - return ApplicabilityResult.NotApplicable( - ApplicabilityFailure.TooManyArguments(parameters.size, arguments.size) - ) - } - - val paramType = if (param.isVararg && !argument.isSpread) { - resolveVarargElementType(param, function, scope) - } else { - resolveParamType(param, function, scope) - } - - if (!isArgumentCompatible(argument, paramType)) { - return ApplicabilityResult.NotApplicable( - ApplicabilityFailure.TypeMismatch(positionalIndex, paramType, argument.type) - ) - } - - if (param.isVararg) { - varargElements.add(argIndex) - } else { - positionalMappings[positionalIndex] = argIndex - usedParams.add(positionalIndex) - positionalIndex++ - } - } - } - - val defaultsUsed = mutableSetOf() - for ((paramIndex, param) in parameters.withIndex()) { - if (paramIndex !in usedParams && !param.isVararg) { - if (!param.hasDefaultValue) { - return ApplicabilityResult.NotApplicable( - ApplicabilityFailure.TooFewArguments( - function.requiredParameterCount, - arguments.size - ) - ) - } - defaultsUsed.add(paramIndex) - } - } - - return ApplicabilityResult.Applicable( - ParameterMapping( - positionalMappings = positionalMappings, - namedMappings = namedMappings, - defaultsUsed = defaultsUsed, - varargElements = varargElements - ) - ) - } - - private fun resolveParamType( - param: ParameterSymbol, - function: FunctionSymbol, - scope: Scope? - ): KotlinType { - return param.type?.let { - context.typeResolver.resolve(it, function.containingScope ?: scope) - } ?: ClassType.ANY - } - - private fun resolveVarargElementType( - param: ParameterSymbol, - function: FunctionSymbol, - scope: Scope? - ): KotlinType { - val arrayType = resolveParamType(param, function, scope) - return when { - arrayType is ClassType && arrayType.fqName == "kotlin.Array" -> { - arrayType.typeArguments.firstOrNull()?.type ?: ClassType.ANY - } - arrayType is ClassType && arrayType.fqName == "kotlin.IntArray" -> { - PrimitiveType.INT - } - arrayType is ClassType && arrayType.fqName == "kotlin.LongArray" -> { - PrimitiveType.LONG - } - else -> ClassType.ANY - } - } - - private fun isArgumentCompatible(argument: CallArgument, paramType: KotlinType): Boolean { - return typeChecker.isSubtypeOf(argument.type, paramType) - } - - private fun rankCandidates( - candidates: List>, - arguments: List, - expectedReturnType: KotlinType?, - scope: Scope? - ): List> { - return candidates.sortedWith { (f1, m1), (f2, m2) -> - val specificityCompare = compareSpecificity(f1, f2, arguments, scope) - if (specificityCompare != 0) { - return@sortedWith -specificityCompare - } - - val defaultsCompare = m1.defaultsUsed.size.compareTo(m2.defaultsUsed.size) - if (defaultsCompare != 0) { - return@sortedWith defaultsCompare - } - - if (expectedReturnType != null) { - val r1 = f1.returnType?.let { context.typeResolver.resolve(it, f1.containingScope ?: scope) } - val r2 = f2.returnType?.let { context.typeResolver.resolve(it, f2.containingScope ?: scope) } - - val r1Match = r1?.let { typeChecker.isSubtypeOf(it, expectedReturnType) } ?: false - val r2Match = r2?.let { typeChecker.isSubtypeOf(it, expectedReturnType) } ?: false - - if (r1Match && !r2Match) return@sortedWith -1 - if (r2Match && !r1Match) return@sortedWith 1 - } - - val isExtension1 = f1.isExtension - val isExtension2 = f2.isExtension - if (!isExtension1 && isExtension2) return@sortedWith -1 - if (isExtension1 && !isExtension2) return@sortedWith 1 - - 0 - } - } - - private fun compareSpecificity( - f1: FunctionSymbol, - f2: FunctionSymbol, - arguments: List, - scope: Scope? - ): Int { - var f1MoreSpecific = 0 - var f2MoreSpecific = 0 - - val minParams = minOf(f1.parameters.size, f2.parameters.size) - for (i in 0 until minParams) { - val p1Type = resolveParamType(f1.parameters[i], f1, scope) - val p2Type = resolveParamType(f2.parameters[i], f2, scope) - - if (typeChecker.isSubtypeOf(p1Type, p2Type) && !typeChecker.isSubtypeOf(p2Type, p1Type)) { - f1MoreSpecific++ - } else if (typeChecker.isSubtypeOf(p2Type, p1Type) && !typeChecker.isSubtypeOf(p1Type, p2Type)) { - f2MoreSpecific++ - } - } - - return when { - f1MoreSpecific > 0 && f2MoreSpecific == 0 -> 1 - f2MoreSpecific > 0 && f1MoreSpecific == 0 -> -1 - else -> 0 - } - } - - private fun isMoreSpecific( - f1: FunctionSymbol, - f2: FunctionSymbol, - arguments: List, - scope: Scope? - ): Boolean { - if (f1 === f2) return true - return compareSpecificity(f1, f2, arguments, scope) >= 0 - } - - /** - * Generates a descriptive error message for failed overload resolution. - */ - fun formatError(result: OverloadResolutionResult): String { - return when (result) { - is OverloadResolutionResult.Success -> "Resolution successful" - is OverloadResolutionResult.Ambiguity -> { - val candidateList = result.candidates.joinToString("\n ") { it.toString() } - "Overload resolution ambiguity between:\n $candidateList" - } - is OverloadResolutionResult.NoApplicable -> { - if (result.diagnostics.isEmpty()) { - "No applicable candidates found" - } else { - val details = result.diagnostics.joinToString("\n ") { diag -> - "${diag.candidate.name}: ${formatFailure(diag.reason)}" - } - "None of the following candidates are applicable:\n $details" - } - } - } - } - - private fun formatFailure(failure: ApplicabilityFailure): String { - return when (failure) { - is ApplicabilityFailure.TooFewArguments -> { - "Too few arguments: expected ${failure.required}, got ${failure.provided}" - } - is ApplicabilityFailure.TooManyArguments -> { - "Too many arguments: expected at most ${failure.max}, got ${failure.provided}" - } - is ApplicabilityFailure.TypeMismatch -> { - "Type mismatch at parameter ${failure.paramIndex}: expected ${failure.expected.render()}, got ${failure.actual.render()}" - } - is ApplicabilityFailure.NamedParameterNotFound -> { - "No parameter named '${failure.name}'" - } - is ApplicabilityFailure.DuplicateNamedArgument -> { - "Duplicate named argument '${failure.name}'" - } - is ApplicabilityFailure.PositionalAfterNamed -> { - "Positional argument at index ${failure.argIndex} after named arguments" - } - } - } - - companion object { - /** - * Creates arguments from a list of types (all positional, no names). - */ - fun argumentsFromTypes(types: List): List { - return types.map { CallArgument(it) } - } - - /** - * Creates a single named argument. - */ - fun namedArgument(name: String, type: KotlinType): CallArgument { - return CallArgument(type, name) - } - - /** - * Creates a spread argument. - */ - fun spreadArgument(type: KotlinType): CallArgument { - return CallArgument(type, isSpread = true) - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SemanticAnalyzer.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SemanticAnalyzer.kt deleted file mode 100644 index e4af2ecb69..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SemanticAnalyzer.kt +++ /dev/null @@ -1,915 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.semantic - -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxKind -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxNode -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassKind -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ImportInfo -import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.PropertySymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Scope -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.types.ClassType -import org.appdevforall.codeonthego.lsp.kotlin.types.KotlinType -import org.appdevforall.codeonthego.lsp.kotlin.types.PrimitiveType - -class SemanticAnalyzer( - private val context: AnalysisContext -) { - private val typeInferrer = TypeInferrer(context) - private val symbolResolver = SymbolResolver(context) - - fun analyze() { - validateImports() - analyzeNode(context.tree.root, context.fileScope) - } - - private fun validateImports() { - for (import in context.symbolTable.imports) { - if (import.isStar) { - validateStarImport(import) - } else { - validateExplicitImport(import) - } - } - } - - private fun validateExplicitImport(import: ImportInfo) { - val fqName = import.fqName - if (canResolveImport(fqName)) { - return - } - - val packagePart = fqName.substringBeforeLast('.', "") - val memberName = fqName.substringAfterLast('.') - - if (packagePart.isNotEmpty()) { - val containerClass = context.projectIndex?.findByFqName(packagePart) - ?: context.stdlibIndex?.findByFqName(packagePart) - - if (containerClass != null) { - val members = when { - context.projectIndex?.getClasspathIndex() != null -> - context.projectIndex.getClasspathIndex()?.findMembers(packagePart) ?: emptyList() - else -> emptyList() - } - if (members.any { it.name == memberName }) { - return - } - } - } - - context.diagnostics.error( - DiagnosticCode.UNRESOLVED_REFERENCE, - import.range, - fqName, - filePath = context.filePath - ) - } - - private fun validateStarImport(import: ImportInfo) { - val packageName = import.fqName - val hasPackage = context.projectIndex?.findByPackage(packageName)?.isNotEmpty() == true || - context.stdlibIndex?.findByPackage(packageName)?.isNotEmpty() == true - - if (!hasPackage) { - val hasClass = context.projectIndex?.findByFqName(packageName) != null || - context.stdlibIndex?.findByFqName(packageName) != null - - if (!hasClass) { - context.diagnostics.warning( - DiagnosticCode.UNRESOLVED_REFERENCE, - import.range, - "$packageName.*", - filePath = context.filePath - ) - } - } - } - - private fun canResolveImport(fqName: String): Boolean { - if (context.stdlibIndex?.findByFqName(fqName) != null) return true - if (context.projectIndex?.findByFqName(fqName) != null) return true - if (context.projectIndex?.getClasspathIndex()?.findByFqName(fqName) != null) return true - - val simpleName = fqName.substringAfterLast('.') - val fileSymbol = context.symbolTable.topLevelSymbols.find { it.name == simpleName } - if (fileSymbol != null) { - val filePackage = context.symbolTable.packageName - val expectedFqName = if (filePackage.isNotEmpty()) "$filePackage.$simpleName" else simpleName - if (expectedFqName == fqName) return true - } - - return false - } - - private fun analyzeNode(node: SyntaxNode, scope: Scope) { - when (node.kind) { - SyntaxKind.FUNCTION_DECLARATION -> analyzeFunctionDeclaration(node, scope) - SyntaxKind.PROPERTY_DECLARATION -> analyzePropertyDeclaration(node, scope) - SyntaxKind.CLASS_DECLARATION, - SyntaxKind.OBJECT_DECLARATION, - SyntaxKind.INTERFACE_DECLARATION -> analyzeClassLikeDeclaration(node, scope) - else -> { - for (child in node.namedChildren) { - analyzeNode(child, scope) - } - } - } - } - - private fun analyzeFunctionDeclaration(node: SyntaxNode, scope: Scope) { - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - val name = nameNode?.text ?: return - - var symbol = scope.resolveFirst(name) as? FunctionSymbol - var functionScope = symbol?.bodyScope ?: scope - - if (symbol == null) { - for (topLevelSymbol in context.symbolTable.topLevelSymbols) { - if (topLevelSymbol is ClassSymbol) { - val classMemberScope = topLevelSymbol.memberScope ?: continue - val funcInClass = classMemberScope.resolveFirst(name) as? FunctionSymbol - if (funcInClass != null) { - symbol = funcInClass - functionScope = funcInClass.bodyScope ?: classMemberScope - break - } - } - } - } - - android.util.Log.d("SemanticAnalyzer", "analyzeFunctionDeclaration: name=$name, symbol=${symbol?.name}, bodyScope=${symbol?.bodyScope}") - android.util.Log.d("SemanticAnalyzer", " functionScope symbols: ${functionScope.allSymbols.map { it.name }}") - android.util.Log.d("SemanticAnalyzer", " functionScope.owner: ${functionScope.owner?.name}") - - val body = node.childByFieldName("body") - ?: node.findChild(SyntaxKind.FUNCTION_BODY) - ?: node.findChild(SyntaxKind.CONTROL_STRUCTURE_BODY) - - if (body != null) { - analyzeBlock(body, functionScope) - } - } - - private fun analyzePropertyDeclaration(node: SyntaxNode, scope: Scope) { - val isDestructuring = node.findChild(SyntaxKind.MULTI_VARIABLE_DECLARATION) != null || - node.children.any { it.text.startsWith("(") && it.text.contains(",") } - - if (isDestructuring) { - val initializer = node.childByFieldName("value") - ?: findInitializerExpression(node, null, null) - if (initializer != null) { - typeInferrer.inferType(initializer, scope) - } - return - } - - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - val propertyName = nameNode?.text - - val typeAnnotation = node.childByFieldName("type") - ?: node.findChild(SyntaxKind.USER_TYPE) - ?: node.findChild(SyntaxKind.NULLABLE_TYPE) - - val initializer = node.childByFieldName("value") - ?: node.findChild(SyntaxKind.DELEGATE) - ?: findInitializerExpression(node, nameNode, typeAnnotation) - - val getter = node.findChild(SyntaxKind.GETTER) - val setter = node.findChild(SyntaxKind.SETTER) - - if (typeAnnotation == null && initializer == null && getter == null && nameNode != null) { - context.reportError(DiagnosticCode.VARIABLE_WITH_NO_TYPE_NO_INITIALIZER, nameNode) - } - - if (initializer != null) { - if (propertyName != null) { - checkForSelfReference(initializer, propertyName, scope) - } - typeInferrer.inferType(initializer, scope) - } - - getter?.let { analyzeAccessor(it, scope) } - setter?.let { analyzeAccessor(it, scope) } - } - - private fun findInitializerExpression( - node: SyntaxNode, - nameNode: SyntaxNode?, - typeAnnotation: SyntaxNode? - ): SyntaxNode? { - val skipKinds = setOf( - SyntaxKind.VAL, - SyntaxKind.VAR, - SyntaxKind.MODIFIERS, - SyntaxKind.VISIBILITY_MODIFIER, - SyntaxKind.GETTER, - SyntaxKind.SETTER, - SyntaxKind.TYPE_PARAMETERS, - SyntaxKind.USER_TYPE, - SyntaxKind.NULLABLE_TYPE - ) - - for (child in node.namedChildren) { - if (nameNode != null && child.range == nameNode.range) continue - if (typeAnnotation != null && child.range == typeAnnotation.range) continue - if (child.kind in skipKinds) continue - if (isExpression(child.kind)) { - return child - } - } - return null - } - - private fun checkForSelfReference(node: SyntaxNode, propertyName: String, scope: Scope) { - when (node.kind) { - SyntaxKind.SIMPLE_IDENTIFIER -> { - if (node.text == propertyName) { - context.reportError( - DiagnosticCode.UNINITIALIZED_VARIABLE, - node, - propertyName - ) - } - } - else -> { - for (child in node.namedChildren) { - checkForSelfReference(child, propertyName, scope) - } - } - } - } - - private fun analyzeAccessor(node: SyntaxNode, scope: Scope) { - val body = node.childByFieldName("body") - ?: node.findChild(SyntaxKind.CONTROL_STRUCTURE_BODY) - - if (body != null) { - analyzeBlock(body, scope) - } - } - - private fun analyzeClassLikeDeclaration(node: SyntaxNode, scope: Scope) { - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: node.findChild(SyntaxKind.TYPE_IDENTIFIER) - val name = nameNode?.text ?: return - - val symbol = scope.resolveFirst(name) - val classSymbol = symbol as? ClassSymbol - val classScope = classSymbol?.memberScope ?: scope - - val body = node.childByFieldName("body") - ?: node.findChild(SyntaxKind.CLASS_BODY) - - if (body != null) { - for (member in body.namedChildren) { - analyzeNode(member, classScope) - } - } - - if (classSymbol != null && nameNode != null) { - checkAbstractMemberImplementation(classSymbol, nameNode) - } - } - - private fun checkAbstractMemberImplementation(classSymbol: ClassSymbol, nameNode: SyntaxNode) { - if (classSymbol.modifiers.isAbstract || classSymbol.isInterface) { - return - } - - val abstractMembers = collectAbstractMembers(classSymbol) - val implementedMembers = collectImplementedMembers(classSymbol) - - for (abstractMember in abstractMembers) { - val memberName = abstractMember.name - val isImplemented = implementedMembers.any { it.name == memberName } - if (!isImplemented) { - val memberDescription = when (abstractMember) { - is FunctionSymbol -> "fun ${abstractMember.name}(...)" - is PropertySymbol -> if (abstractMember.isVar) "var ${abstractMember.name}" else "val ${abstractMember.name}" - else -> abstractMember.name - } - context.reportError( - DiagnosticCode.ABSTRACT_MEMBER_NOT_IMPLEMENTED, - nameNode, - classSymbol.name, - memberDescription - ) - } - } - } - - private fun collectAbstractMembers(classSymbol: ClassSymbol): List { - val abstractMembers = mutableMapOf() - - for (superTypeRef in classSymbol.superTypes) { - val superClass = resolveClassSymbol(superTypeRef.name, classSymbol.containingScope ?: context.fileScope) - if (superClass != null) { - for (member in superClass.members) { - val isAbstract = when (member) { - is FunctionSymbol -> { - member.modifiers.isAbstract || (superClass.isInterface && !member.hasBody) - } - is PropertySymbol -> { - member.modifiers.isAbstract || (superClass.isInterface && !member.hasInitializer && member.getter == null) - } - else -> false - } - if (isAbstract && member.name !in abstractMembers) { - abstractMembers[member.name] = member - } - } - } - } - - return abstractMembers.values.toList() - } - - private fun collectImplementedMembers(classSymbol: ClassSymbol): List { - return classSymbol.members.filter { member -> - when (member) { - is FunctionSymbol -> !member.modifiers.isAbstract - is PropertySymbol -> !member.modifiers.isAbstract - else -> true - } - } - } - - private fun analyzeBlock(node: SyntaxNode, scope: Scope) { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] analyzeBlock: node.kind=${node.kind}") - val statements = when (node.kind) { - SyntaxKind.FUNCTION_BODY -> { - val block = node.findChild(SyntaxKind.STATEMENTS) - block?.namedChildren ?: node.namedChildren - } - SyntaxKind.CONTROL_STRUCTURE_BODY -> { - val block = node.findChild(SyntaxKind.STATEMENTS) - block?.namedChildren ?: node.namedChildren - } - SyntaxKind.STATEMENTS -> node.namedChildren - else -> node.namedChildren - } - - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] analyzeBlock found ${statements.size} statements: ${statements.map { "${it.kind}:${it.text.take(20)}" }}") - for (statement in statements) { - analyzeStatement(statement, scope) - } - } - - private fun analyzeStatement(node: SyntaxNode, scope: Scope) { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] analyzeStatement: kind=${node.kind}, text='${node.text.take(40)}'") - when (node.kind) { - SyntaxKind.PROPERTY_DECLARATION -> analyzePropertyDeclaration(node, scope) - SyntaxKind.ASSIGNMENT -> { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] Found ASSIGNMENT node!") - analyzeAssignment(node, scope) - } - SyntaxKind.AUGMENTED_ASSIGNMENT -> analyzeAugmentedAssignment(node, scope) - SyntaxKind.RETURN -> analyzeReturn(node, scope) - SyntaxKind.IF_EXPRESSION -> analyzeIfExpression(node, scope) - SyntaxKind.WHEN_EXPRESSION -> analyzeWhenExpression(node, scope) - SyntaxKind.FOR_STATEMENT -> analyzeForStatement(node, scope) - SyntaxKind.WHILE_STATEMENT -> analyzeWhileStatement(node, scope) - SyntaxKind.DO_WHILE_STATEMENT -> analyzeDoWhileStatement(node, scope) - SyntaxKind.TRY_EXPRESSION -> analyzeTryExpression(node, scope) - else -> { - if (isExpression(node.kind)) { - typeInferrer.inferType(node, scope) - } else { - for (child in node.namedChildren) { - analyzeStatement(child, scope) - } - } - } - } - } - - private fun analyzeAssignment(node: SyntaxNode, scope: Scope) { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] analyzeAssignment called: node.text='${node.text.take(50)}', namedChildren=${node.namedChildren.map { "${it.kind}:${it.text.take(20)}" }}") - val target = node.childByFieldName("left") ?: node.namedChildren.getOrNull(0) - val value = node.childByFieldName("right") ?: node.namedChildren.getOrNull(1) - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] target=${target?.kind}:${target?.text?.take(20)}, value=${value?.kind}:${value?.text?.take(20)}") - - var targetType: KotlinType? = null - if (target != null) { - targetType = typeInferrer.inferType(target, scope) - checkValReassignment(target, scope) - } - if (value != null) { - if (value.kind == SyntaxKind.JUMP_EXPRESSION) { - context.reportError(DiagnosticCode.UNREACHABLE_CODE, node) - } else { - val valueType = typeInferrer.inferType(value, scope) - if (targetType != null && !targetType.hasError && !valueType.hasError) { - checkAssignmentTypeCompatibility(targetType, valueType, value) - } - } - } - } - - private fun checkAssignmentTypeCompatibility( - targetType: KotlinType, - valueType: KotlinType, - valueNode: SyntaxNode - ) { - if (!context.typeChecker.isAssignableTo(valueType, targetType)) { - context.reportError( - DiagnosticCode.TYPE_MISMATCH, - valueNode, - targetType.render(), - valueType.render() - ) - } - } - - private fun analyzeAugmentedAssignment(node: SyntaxNode, scope: Scope) { - val target = node.childByFieldName("left") ?: node.namedChildren.getOrNull(0) - val value = node.childByFieldName("right") ?: node.namedChildren.getOrNull(1) - - var targetType: KotlinType? = null - if (target != null) { - targetType = typeInferrer.inferType(target, scope) - checkValReassignment(target, scope) - } - if (value != null) { - val valueType = typeInferrer.inferType(value, scope) - if (targetType != null && !targetType.hasError && !valueType.hasError) { - checkAssignmentTypeCompatibility(targetType, valueType, value) - } - } - } - - private fun checkValReassignment(target: SyntaxNode, scope: Scope) { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] checkValReassignment: target.kind=${target.kind}, target.text='${target.text.take(50)}'") - val symbol = extractAssignableSymbol(target, scope) - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] extractAssignableSymbol returned: ${symbol?.name}, isVar=${symbol?.isVar}") - if (symbol == null) { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] symbol is null, skipping check") - return - } - if (!symbol.isVar) { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] REPORTING VAL_REASSIGNMENT for '${symbol.name}'") - context.reportError(DiagnosticCode.VAL_REASSIGNMENT, target) - } else { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] symbol '${symbol.name}' is var, no error") - } - } - - private fun extractAssignableSymbol(node: SyntaxNode, scope: Scope): PropertySymbol? { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] extractAssignableSymbol: node.kind=${node.kind}, text='${node.text.take(30)}'") - return when (node.kind) { - SyntaxKind.DIRECTLY_ASSIGNABLE_EXPRESSION -> { - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] DIRECTLY_ASSIGNABLE_EXPRESSION, children=${node.namedChildren.map { "${it.kind}:${it.text.take(20)}" }}") - val inner = node.namedChildren.firstOrNull() - if (inner != null) extractAssignableSymbol(inner, scope) else null - } - SyntaxKind.SIMPLE_IDENTIFIER -> { - val resolved = scope.resolveFirst(node.text) - android.util.Log.d("SemanticAnalyzer", "[VAL-CHECK] SIMPLE_IDENTIFIER '${node.text}' resolved to: $resolved (type=${resolved?.javaClass?.simpleName})") - resolved as? PropertySymbol - } - SyntaxKind.NAVIGATION_EXPRESSION -> { - val receiver = node.childByFieldName("receiver") ?: node.namedChildren.firstOrNull() - val suffix = node.childByFieldName("suffix") - ?: node.findChild(SyntaxKind.NAVIGATION_SUFFIX)?.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: node.namedChildren.lastOrNull { it.kind == SyntaxKind.SIMPLE_IDENTIFIER && it != receiver } - - if (receiver != null && suffix != null) { - val receiverType = typeInferrer.inferType(receiver, scope) - val result = symbolResolver.resolveMemberAccess(receiverType, suffix.text, scope) - if (result is SymbolResolver.ResolutionResult.Resolved) { - result.symbol as? PropertySymbol - } else null - } else null - } - SyntaxKind.INDEXING_EXPRESSION -> { - val receiver = node.namedChildren.firstOrNull() - if (receiver != null) extractAssignableSymbol(receiver, scope) else null - } - SyntaxKind.PARENTHESIZED_EXPRESSION -> { - val inner = node.namedChildren.firstOrNull() - if (inner != null) extractAssignableSymbol(inner, scope) else null - } - else -> null - } - } - - private fun analyzeReturn(node: SyntaxNode, scope: Scope) { - val expr = node.namedChildren.firstOrNull() - if (expr != null) { - typeInferrer.inferType(expr, scope) - } - } - - private fun analyzeIfExpression(node: SyntaxNode, scope: Scope) { - val condition = node.childByFieldName("condition") - val thenBranch = node.childByFieldName("consequence") - val elseBranch = node.childByFieldName("alternative") - - if (condition != null) { - val conditionType = typeInferrer.inferType(condition, scope) - checkConditionType(conditionType, condition) - } - - val smartCastInfo = extractSmartCastFromCondition(condition, scope) - - if (thenBranch != null) { - if (smartCastInfo != null) { - context.pushSmartCast(smartCastInfo.first, smartCastInfo.second) - analyzeBlock(thenBranch, scope) - context.popSmartCast(smartCastInfo.first) - } else { - analyzeBlock(thenBranch, scope) - } - } - elseBranch?.let { analyzeBlock(it, scope) } - } - - private fun extractSmartCastFromCondition(condition: SyntaxNode?, scope: Scope): Pair? { - if (condition == null) return null - - val checkExpr = when (condition.kind) { - SyntaxKind.CHECK_EXPRESSION -> condition - SyntaxKind.PARENTHESIZED_EXPRESSION -> { - condition.namedChildren.firstOrNull()?.let { inner -> - if (inner.kind == SyntaxKind.CHECK_EXPRESSION) inner else null - } - } - else -> condition.findChild(SyntaxKind.CHECK_EXPRESSION) - } ?: return null - - val isOperator = checkExpr.children.find { it.text == "is" || it.text == "!is" } - if (isOperator?.text != "is") return null - - val subjectNode = checkExpr.namedChildren.firstOrNull() ?: return null - val typeNode = checkExpr.findChild(SyntaxKind.USER_TYPE) - ?: checkExpr.findChild(SyntaxKind.NULLABLE_TYPE) - ?: return null - - val symbol = extractSubjectSymbol(subjectNode, scope) ?: return null - val originalType = typeInferrer.inferType(subjectNode, scope) - val castType = extractTypeFromNode(typeNode, scope) ?: return null - - return symbol to SmartCastInfo(originalType, castType, condition) - } - - private fun extractTypeFromNode(typeNode: SyntaxNode, scope: Scope): KotlinType? { - val fullTypeName = extractFullTypeName(typeNode) - - if ('.' in fullTypeName) { - val parts = fullTypeName.split('.') - val outerClassName = parts.first() - val innerNames = parts.drop(1) - - val outerClass = scope.resolve(outerClassName).filterIsInstance().firstOrNull() - ?: context.symbolTable.fileScope.resolve(outerClassName).filterIsInstance().firstOrNull() - - if (outerClass != null) { - var currentClass: ClassSymbol = outerClass - for (innerName in innerNames) { - val innerClass = currentClass.members.filterIsInstance() - .find { it.name == innerName } - if (innerClass != null) { - currentClass = innerClass - } else { - break - } - } - return ClassType(currentClass.qualifiedName) - } - } - - val classSymbol = scope.resolve(fullTypeName).filterIsInstance().firstOrNull() - ?: context.symbolTable.fileScope.resolve(fullTypeName).filterIsInstance().firstOrNull() - - return if (classSymbol != null) { - ClassType(classSymbol.qualifiedName) - } else { - ClassType(fullTypeName) - } - } - - private fun analyzeWhenExpression(node: SyntaxNode, scope: Scope) { - val subject = node.childByFieldName("subject") - val subjectType = subject?.let { typeInferrer.inferType(it, scope) } - val subjectSymbol = extractSubjectSymbol(subject, scope) - - val entries = node.findChildren(SyntaxKind.WHEN_ENTRY) - var hasElseBranch = false - val coveredCases = mutableSetOf() - - for (entry in entries) { - val conditions = entry.findChildren(SyntaxKind.WHEN_CONDITION) - - if (conditions.isEmpty()) { - hasElseBranch = true - } - - var smartCastType: KotlinType? = null - for (condition in conditions) { - for (child in condition.namedChildren) { - typeInferrer.inferType(child, scope) - - if (child.kind == SyntaxKind.SIMPLE_IDENTIFIER) { - coveredCases.add(child.text) - } else if (child.kind == SyntaxKind.NAVIGATION_EXPRESSION) { - coveredCases.add(child.text) - } else if (child.kind == SyntaxKind.TYPE_TEST) { - val castType = extractTypeTestType(child, scope) - if (castType != null) { - smartCastType = castType - val typeName = child.findChild(SyntaxKind.USER_TYPE)?.text - ?: child.findChild(SyntaxKind.SIMPLE_IDENTIFIER)?.text - if (typeName != null) { - coveredCases.add(typeName) - } - } - } - } - } - - val body = entry.findChild(SyntaxKind.CONTROL_STRUCTURE_BODY) - if (body != null) { - if (subjectSymbol != null && subjectType != null && smartCastType != null) { - context.pushSmartCast(subjectSymbol, SmartCastInfo(subjectType, smartCastType, entry), body) - analyzeBlock(body, scope) - context.popSmartCast(subjectSymbol) - } else { - analyzeBlock(body, scope) - } - } - } - - val isUsedAsExpression = isWhenUsedAsExpression(node) - if (isUsedAsExpression && !hasElseBranch && subjectType != null) { - checkWhenExhaustiveness(node, subjectType, coveredCases, scope) - } - } - - private fun isWhenUsedAsExpression(node: SyntaxNode): Boolean { - val parent = node.parent ?: return false - return when (parent.kind) { - SyntaxKind.PROPERTY_DECLARATION, - SyntaxKind.ASSIGNMENT, - SyntaxKind.VALUE_ARGUMENT, - SyntaxKind.RETURN -> true - else -> false - } - } - - private fun checkWhenExhaustiveness( - node: SyntaxNode, - subjectType: KotlinType, - coveredCases: Set, - scope: Scope - ) { - if (subjectType !is ClassType) return - - val classSymbol = resolveClassSymbol(subjectType.fqName, scope) ?: return - - when { - classSymbol.kind == ClassKind.ENUM_CLASS -> { - val enumEntries = classSymbol.members - .filterIsInstance() - .filter { it.kind == ClassKind.ENUM_ENTRY } - .map { it.name } - .toSet() - - val missingCases = enumEntries - coveredCases - if (missingCases.isNotEmpty()) { - val missing = missingCases.joinToString(", ") { "'$it'" } - context.reportError(DiagnosticCode.MISSING_WHEN_BRANCH, node, missing) - } - } - classSymbol.modifiers.isSealed -> { - val subclasses = findSealedSubclasses(classSymbol, scope) - val subclassNames = subclasses.map { it.name }.toSet() - val missingCases = subclassNames - coveredCases - if (missingCases.isNotEmpty()) { - val missing = missingCases.joinToString(", ") { "'$it'" } - context.reportError(DiagnosticCode.MISSING_WHEN_BRANCH, node, missing) - } - } - subjectType.fqName == "kotlin.Boolean" -> { - val booleanCases = setOf("true", "false") - val missingCases = booleanCases - coveredCases - if (missingCases.isNotEmpty()) { - val missing = missingCases.joinToString(", ") { "'$it'" } - context.reportError(DiagnosticCode.MISSING_WHEN_BRANCH, node, missing) - } - } - } - } - - private fun resolveClassSymbol(fqName: String, scope: Scope): ClassSymbol? { - val simpleName = fqName.substringAfterLast('.') - return scope.resolve(simpleName).filterIsInstance().firstOrNull() - ?: context.symbolTable.fileScope.resolve(simpleName).filterIsInstance().firstOrNull() - } - - private fun findSealedSubclasses(sealedClass: ClassSymbol, scope: Scope): List { - val result = mutableListOf() - - for (symbol in context.symbolTable.topLevelSymbols) { - if (symbol is ClassSymbol) { - val implementsSealedClass = symbol.superTypes.any { - it.name == sealedClass.name || it.name == sealedClass.qualifiedName - } - if (implementsSealedClass) { - result.add(symbol) - } - } - } - - context.projectIndex?.getAllClasses() - ?.filter { indexed -> - indexed.superTypes.any { st -> - st == sealedClass.qualifiedName || st == sealedClass.name - } - } - ?.mapNotNull { it.toSyntheticSymbol() as? ClassSymbol } - ?.forEach { if (it.name !in result.map { r -> r.name }) result.add(it) } - - return result - } - - private fun analyzeForStatement(node: SyntaxNode, scope: Scope) { - val iterable = node.childByFieldName("iterable") - val body = node.childByFieldName("body") - - iterable?.let { typeInferrer.inferType(it, scope) } - body?.let { analyzeBlock(it, scope) } - } - - private fun analyzeWhileStatement(node: SyntaxNode, scope: Scope) { - val condition = node.childByFieldName("condition") - val body = node.childByFieldName("body") - - if (condition != null) { - val conditionType = typeInferrer.inferType(condition, scope) - checkConditionType(conditionType, condition) - } - body?.let { analyzeBlock(it, scope) } - } - - private fun analyzeDoWhileStatement(node: SyntaxNode, scope: Scope) { - val body = node.childByFieldName("body") - val condition = node.childByFieldName("condition") - - body?.let { analyzeBlock(it, scope) } - if (condition != null) { - val conditionType = typeInferrer.inferType(condition, scope) - checkConditionType(conditionType, condition) - } - } - - private fun checkConditionType(type: KotlinType, node: SyntaxNode) { - if (type.hasError) return - if (!isBoolean(type)) { - context.reportError(DiagnosticCode.CONDITION_TYPE_MISMATCH, node, type.render()) - } - } - - private fun isBoolean(type: KotlinType): Boolean { - return type == PrimitiveType.BOOLEAN || - (type is ClassType && type.fqName == "kotlin.Boolean") - } - - private fun analyzeTryExpression(node: SyntaxNode, scope: Scope) { - val tryBlock = node.childByFieldName("body") - tryBlock?.let { analyzeBlock(it, scope) } - - for (catchClause in node.findChildren(SyntaxKind.CATCH_BLOCK)) { - val catchBody = catchClause.childByFieldName("body") - catchBody?.let { analyzeBlock(it, scope) } - } - - val finallyClause = node.findChild(SyntaxKind.FINALLY_BLOCK) - val finallyBody = finallyClause?.childByFieldName("body") - finallyBody?.let { analyzeBlock(it, scope) } - } - - private fun isExpression(kind: SyntaxKind): Boolean { - return kind in setOf( - SyntaxKind.CALL_EXPRESSION, - SyntaxKind.NAVIGATION_EXPRESSION, - SyntaxKind.SIMPLE_IDENTIFIER, - SyntaxKind.STRING_LITERAL, - SyntaxKind.INTEGER_LITERAL, - SyntaxKind.REAL_LITERAL, - SyntaxKind.BOOLEAN_LITERAL, - SyntaxKind.NULL_LITERAL, - SyntaxKind.BINARY_EXPRESSION, - SyntaxKind.PREFIX_EXPRESSION, - SyntaxKind.POSTFIX_EXPRESSION, - SyntaxKind.PARENTHESIZED_EXPRESSION, - SyntaxKind.INDEXING_EXPRESSION, - SyntaxKind.AS_EXPRESSION, - SyntaxKind.TYPE_TEST, - SyntaxKind.LAMBDA_LITERAL, - SyntaxKind.OBJECT_LITERAL, - SyntaxKind.COLLECTION_LITERAL, - SyntaxKind.THIS_EXPRESSION, - SyntaxKind.SUPER_EXPRESSION, - SyntaxKind.RANGE_EXPRESSION, - SyntaxKind.INFIX_EXPRESSION, - SyntaxKind.ELVIS_EXPRESSION, - SyntaxKind.CHECK_EXPRESSION, - SyntaxKind.COMPARISON_EXPRESSION, - SyntaxKind.EQUALITY_EXPRESSION, - SyntaxKind.CONJUNCTION_EXPRESSION, - SyntaxKind.DISJUNCTION_EXPRESSION, - SyntaxKind.ADDITIVE_EXPRESSION, - SyntaxKind.MULTIPLICATIVE_EXPRESSION - ) - } - - private fun extractSubjectSymbol(subject: SyntaxNode?, scope: Scope): Symbol? { - if (subject == null) return null - - return when (subject.kind) { - SyntaxKind.SIMPLE_IDENTIFIER -> { - scope.resolveFirst(subject.text) - } - SyntaxKind.PARENTHESIZED_EXPRESSION -> { - val inner = subject.namedChildren.firstOrNull() - extractSubjectSymbol(inner, scope) - } - else -> { - val innerIdentifier = subject.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - if (innerIdentifier != null) { - scope.resolveFirst(innerIdentifier.text) - } else { - null - } - } - } - } - - private fun extractTypeTestType(typeTest: SyntaxNode, scope: Scope): KotlinType? { - val typeNode = typeTest.findChild(SyntaxKind.USER_TYPE) - ?: typeTest.findChild(SyntaxKind.NULLABLE_TYPE) - ?: return null - - val fullTypeName = extractFullTypeName(typeNode) - - if ('.' in fullTypeName) { - val parts = fullTypeName.split('.') - val outerClassName = parts.first() - val innerNames = parts.drop(1) - - val outerClass = scope.resolve(outerClassName).filterIsInstance().firstOrNull() - ?: context.symbolTable.fileScope.resolve(outerClassName).filterIsInstance().firstOrNull() - - if (outerClass != null) { - var currentClass: ClassSymbol = outerClass - for (innerName in innerNames) { - val innerClass = currentClass.members.filterIsInstance() - .find { it.name == innerName } - if (innerClass != null) { - currentClass = innerClass - } else { - break - } - } - return ClassType(currentClass.qualifiedName) - } - } - - val classSymbol = scope.resolve(fullTypeName).filterIsInstance().firstOrNull() - ?: context.symbolTable.fileScope.resolve(fullTypeName).filterIsInstance().firstOrNull() - - return if (classSymbol != null) { - ClassType(classSymbol.qualifiedName) - } else { - ClassType(fullTypeName) - } - } - - private fun extractFullTypeName(typeNode: SyntaxNode): String { - val identifiers = mutableListOf() - - fun collectIdentifiers(node: SyntaxNode) { - when (node.kind) { - SyntaxKind.SIMPLE_IDENTIFIER -> identifiers.add(node.text) - SyntaxKind.USER_TYPE, SyntaxKind.SIMPLE_USER_TYPE -> { - for (child in node.namedChildren) { - collectIdentifiers(child) - } - } - else -> { - for (child in node.namedChildren) { - collectIdentifiers(child) - } - } - } - } - - collectIdentifiers(typeNode) - return if (identifiers.isNotEmpty()) identifiers.joinToString(".") else typeNode.text - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SymbolResolver.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SymbolResolver.kt deleted file mode 100644 index c29e350bdf..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/SymbolResolver.kt +++ /dev/null @@ -1,1116 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.semantic - -import android.util.Log -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbolKind -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxKind -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxNode -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ImportInfo -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Modifiers -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.PropertySymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Scope -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolLocation -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeReference -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeAliasSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.types.ClassType -import org.appdevforall.codeonthego.lsp.kotlin.types.ErrorType -import org.appdevforall.codeonthego.lsp.kotlin.types.FunctionType -import org.appdevforall.codeonthego.lsp.kotlin.types.KotlinType -import org.appdevforall.codeonthego.lsp.kotlin.types.PrimitiveType -import org.appdevforall.codeonthego.lsp.kotlin.types.TypeParameter -import org.appdevforall.codeonthego.lsp.kotlin.types.TypeChecker - -private const val TAG = "SymbolResolver" - -/** - * Resolves name references to their corresponding symbols. - * - * SymbolResolver handles all forms of name resolution in Kotlin: - * - Simple names via scope chain lookup - * - Qualified names (package.Class.member) - * - Member access on expressions (expr.member) - * - Import-based resolution - * - Extension function/property lookup - * - * ## Resolution Algorithm - * - * For simple names: - * 1. Search current scope up through parent scopes - * 2. Check imports (explicit and star imports) - * 3. Check implicitly imported packages (kotlin.*, kotlin.collections.*, etc.) - * - * For member access: - * 1. Infer receiver type - * 2. Search receiver type's members - * 3. Search extension functions/properties applicable to receiver type - * - * ## Usage - * - * ```kotlin - * val resolver = SymbolResolver(context) - * val symbol = resolver.resolveSimpleName("foo", scope) - * val member = resolver.resolveMemberAccess(receiverType, "bar", scope) - * ``` - */ -class SymbolResolver( - private val context: AnalysisContext -) { - private val typeChecker: TypeChecker = context.typeChecker - private val symbolTable: SymbolTable = context.symbolTable - - private val implicitImports = listOf( - "kotlin", - "kotlin.annotation", - "kotlin.collections", - "kotlin.comparisons", - "kotlin.io", - "kotlin.ranges", - "kotlin.sequences", - "kotlin.text" - ) - - private val stdlibIndex = context.stdlibIndex - private val projectIndex = context.projectIndex - - /** - * Result of symbol resolution. - */ - sealed class ResolutionResult { - /** - * Successfully resolved to one or more symbols. - * Multiple symbols indicate overloaded functions. - */ - data class Resolved(val symbols: List) : ResolutionResult() { - val symbol: Symbol get() = symbols.first() - val isAmbiguous: Boolean get() = symbols.size > 1 && symbols.none { it is FunctionSymbol } - } - - /** - * Resolution failed - symbol not found. - */ - data class Unresolved(val name: String) : ResolutionResult() - - /** - * Resolution found multiple incompatible candidates. - */ - data class Ambiguous(val candidates: List) : ResolutionResult() - - val isResolved: Boolean get() = this is Resolved - val isUnresolved: Boolean get() = this is Unresolved - } - - /** - * Resolves a simple name in the given scope. - * - * @param name The name to resolve - * @param scope The scope to start resolution from - * @return Resolution result with found symbols or error - */ - fun resolveSimpleName(name: String, scope: Scope): ResolutionResult { - val scopeResult = scope.resolve(name) - if (scopeResult.isNotEmpty()) { - Log.d(TAG, "resolveSimpleName: '$name' found in scope: ${scopeResult.map { it::class.simpleName }}") - return ResolutionResult.Resolved(scopeResult) - } - - val importResult = resolveViaImports(name) - if (importResult.isNotEmpty()) { - Log.d(TAG, "resolveSimpleName: '$name' found via imports") - return ResolutionResult.Resolved(importResult) - } - - val implicitResult = resolveViaImplicitImports(name) - if (implicitResult.isNotEmpty()) { - Log.d(TAG, "resolveSimpleName: '$name' found via implicit imports") - return ResolutionResult.Resolved(implicitResult) - } - - val projectResult = resolveViaProjectIndex(name) - if (projectResult.isNotEmpty()) { - Log.d(TAG, "resolveSimpleName: '$name' found via project index") - return ResolutionResult.Resolved(projectResult) - } - - Log.d(TAG, "resolveSimpleName: '$name', checking stdlib (index=${stdlibIndex != null}, size=${stdlibIndex?.size ?: 0})") - val stdlibSymbols = stdlibIndex?.findBySimpleName(name) ?: emptyList() - Log.d(TAG, "resolveSimpleName: '$name' found ${stdlibSymbols.size} stdlib symbols") - if (stdlibSymbols.isNotEmpty()) { - return ResolutionResult.Resolved(stdlibSymbols.map { it.toSyntheticSymbol() }) - } - - val classpathIndex = projectIndex?.getClasspathIndex() - Log.d(TAG, "resolveSimpleName: '$name', checking classpath (index=${classpathIndex != null}, size=${classpathIndex?.size ?: 0})") - val classpathSymbols = classpathIndex?.findBySimpleName(name) ?: emptyList() - Log.d(TAG, "resolveSimpleName: '$name' found ${classpathSymbols.size} classpath symbols") - if (classpathSymbols.isNotEmpty()) { - return ResolutionResult.Resolved(classpathSymbols.map { it.toSyntheticSymbol() }) - } - - Log.d(TAG, "resolveSimpleName: '$name' NOT FOUND anywhere") - return ResolutionResult.Unresolved(name) - } - - /** - * Resolves a qualified name (e.g., "kotlin.String", "Foo.Bar.baz"). - * - * @param qualifiedName The full qualified name - * @param scope The scope for resolving the first component - * @return Resolution result - */ - fun resolveQualifiedName(qualifiedName: String, scope: Scope): ResolutionResult { - val parts = qualifiedName.split('.') - if (parts.isEmpty()) { - return ResolutionResult.Unresolved(qualifiedName) - } - - var currentResult = resolveSimpleName(parts.first(), scope) - if (currentResult !is ResolutionResult.Resolved) { - return ResolutionResult.Unresolved(qualifiedName) - } - - for (i in 1 until parts.size) { - val memberName = parts[i] - val currentSymbol = (currentResult as ResolutionResult.Resolved).symbol - - val memberResult = when (currentSymbol) { - is ClassSymbol -> resolveMemberInClass(currentSymbol, memberName) - is PropertySymbol -> { - val propertyType = context.typeResolver.resolve( - currentSymbol.type ?: return ResolutionResult.Unresolved(qualifiedName), - scope - ) - resolveMemberInType(propertyType, memberName, scope) - } - else -> return ResolutionResult.Unresolved(qualifiedName) - } - - if (memberResult !is ResolutionResult.Resolved) { - return ResolutionResult.Unresolved(qualifiedName) - } - currentResult = memberResult - } - - return currentResult - } - - /** - * Resolves a member access expression (expr.member). - * - * @param receiverType The type of the receiver expression - * @param memberName The name of the member being accessed - * @param scope The scope for extension function lookup - * @return Resolution result - */ - fun resolveMemberAccess( - receiverType: KotlinType, - memberName: String, - scope: Scope - ): ResolutionResult { - val members = resolveMemberInType(receiverType, memberName, scope) - if (members is ResolutionResult.Resolved) { - return members - } - - val extensions = resolveExtension(receiverType, memberName, scope) - if (extensions.isNotEmpty()) { - return ResolutionResult.Resolved(extensions) - } - - val qualifiedReceiverType = receiverType.render(qualified = true) - val simpleReceiverType = receiverType.render(qualified = false) - Log.d(TAG, "resolveMemberAccess: member='$memberName', qualifiedReceiver='$qualifiedReceiverType', simpleReceiver='$simpleReceiverType'") - - val stdlibExtensions = stdlibIndex?.findExtensions(qualifiedReceiverType) - ?.filter { it.name == memberName } - ?: emptyList() - Log.d(TAG, "resolveMemberAccess: found ${stdlibExtensions.size} extensions for qualified type") - - if (stdlibExtensions.isNotEmpty()) { - return ResolutionResult.Resolved(stdlibExtensions.map { it.toSyntheticSymbol() }) - } - - val simpleExtensions = stdlibIndex?.findExtensions(simpleReceiverType) - ?.filter { it.name == memberName } - ?: emptyList() - Log.d(TAG, "resolveMemberAccess: found ${simpleExtensions.size} extensions for simple type") - - if (simpleExtensions.isNotEmpty()) { - return ResolutionResult.Resolved(simpleExtensions.map { it.toSyntheticSymbol() }) - } - - val extensionByName = stdlibIndex?.findExtensionsByName(memberName) - ?.firstOrNull() - Log.d(TAG, "resolveMemberAccess: extensionByName for '$memberName' = ${extensionByName != null}") - - if (extensionByName != null) { - return ResolutionResult.Resolved(listOf(extensionByName.toSyntheticSymbol())) - } - - return ResolutionResult.Unresolved(memberName) - } - - /** - * Resolves a member within a type. - */ - private fun resolveMemberInType( - type: KotlinType, - memberName: String, - scope: Scope - ): ResolutionResult { - return when (type) { - is ClassType -> { - Log.d(TAG, "resolveMemberInType: looking up class '${type.fqName}' for member '$memberName'") - val classSymbol = findClassSymbol(type.fqName, scope) - if (classSymbol != null) { - Log.d(TAG, "resolveMemberInType: found classSymbol '${classSymbol.qualifiedName}', isSynthetic=${classSymbol.isSynthetic}") - resolveMemberInClass(classSymbol, memberName) - } else { - Log.d(TAG, "resolveMemberInType: classSymbol not found for '${type.fqName}'") - if ('.' !in type.fqName) { - Log.d(TAG, "resolveMemberInType: trying direct lookup by fqName in classpath for simple name") - val result = resolveMemberInClassByFqName(type.fqName, memberName) - if (result.isResolved) { - return result - } - } - ResolutionResult.Unresolved(memberName) - } - } - is PrimitiveType -> { - val typeName = type.kind.name.lowercase().replaceFirstChar { it.uppercase() } - val boxedClass = findClassSymbol("kotlin.$typeName", scope) - if (boxedClass != null) { - resolveMemberInClass(boxedClass, memberName) - } else { - ResolutionResult.Unresolved(memberName) - } - } - is FunctionType -> { - resolveFunctionTypeMember(type, memberName, scope) - } - is TypeParameter -> { - if (type.hasBounds) { - val boundType = type.effectiveBound - resolveMemberInType(boundType, memberName, scope) - } else { - resolveMemberInType(ClassType.ANY_NULLABLE, memberName, scope) - } - } - else -> ResolutionResult.Unresolved(memberName) - } - } - - /** - * Resolves a member within a class symbol. - */ - private fun resolveMemberInClass(classSymbol: ClassSymbol, memberName: String): ResolutionResult { - Log.d(TAG, "resolveMemberInClass: class=${classSymbol.qualifiedName}, member=$memberName, isSynthetic=${classSymbol.isSynthetic}") - - val directMembers = classSymbol.findMember(memberName) - if (directMembers.isNotEmpty()) { - Log.d(TAG, "resolveMemberInClass: found direct member") - return ResolutionResult.Resolved(directMembers) - } - - if (classSymbol.isSynthetic) { - val classpathIndex = projectIndex?.getClasspathIndex() - if (classpathIndex != null) { - val indexedMembers = classpathIndex.findMembers(classSymbol.qualifiedName) - .filter { it.name == memberName } - if (indexedMembers.isNotEmpty()) { - Log.d(TAG, "resolveMemberInClass: found ${indexedMembers.size} members for '$memberName' in classpath") - return ResolutionResult.Resolved(indexedMembers.map { it.toSyntheticSymbol() }) - } - - val indexedClass = classpathIndex.findByFqName(classSymbol.qualifiedName) - if (indexedClass != null && indexedClass.superTypes.isNotEmpty()) { - Log.d(TAG, "resolveMemberInClass: searching supertypes: ${indexedClass.superTypes}") - for (superTypeFqName in indexedClass.superTypes) { - val superResult = resolveMemberInClassByFqName(superTypeFqName, memberName) - if (superResult is ResolutionResult.Resolved) { - return superResult - } - } - } - } - - val stdlibMembers = stdlibIndex?.findMembers(classSymbol.qualifiedName) - ?.filter { it.name == memberName } - if (stdlibMembers != null && stdlibMembers.isNotEmpty()) { - Log.d(TAG, "resolveMemberInClass: found ${stdlibMembers.size} members for '$memberName' in stdlib") - return ResolutionResult.Resolved(stdlibMembers.map { it.toSyntheticSymbol() }) - } - } - - Log.d(TAG, "resolveMemberInClass: checking ${classSymbol.superTypes.size} supertypes for local class") - val classpathIndex = projectIndex?.getClasspathIndex() - - for (superTypeRef in classSymbol.superTypes) { - val simpleName = superTypeRef.simpleName - Log.d(TAG, "resolveMemberInClass: resolving supertype '$simpleName'") - - var superTypeFqName: String? = null - - val fqNameFromImport = resolveFqNameFromImports(simpleName) - if (fqNameFromImport != null) { - superTypeFqName = fqNameFromImport - Log.d(TAG, "resolveMemberInClass: resolved via import to '$superTypeFqName'") - } - - if (superTypeFqName == null) { - val resolvedType = context.typeResolver.resolve(superTypeRef, classSymbol.containingScope) - if (resolvedType is ClassType && '.' in resolvedType.fqName) { - val typeResolverFqName = resolvedType.fqName - Log.d(TAG, "resolveMemberInClass: TypeResolver resolved to '$typeResolverFqName'") - - val existsInClasspath = classpathIndex?.findByFqName(typeResolverFqName) != null - val existsInStdlib = stdlibIndex?.findByFqName(typeResolverFqName) != null - - if (existsInClasspath || existsInStdlib) { - superTypeFqName = typeResolverFqName - Log.d(TAG, "resolveMemberInClass: TypeResolver result validated (classpath=$existsInClasspath, stdlib=$existsInStdlib)") - } else { - Log.d(TAG, "resolveMemberInClass: TypeResolver result '$typeResolverFqName' not found in classpath/stdlib, ignoring") - } - } - } - - if (superTypeFqName != null && '.' in superTypeFqName) { - val superResult = resolveMemberInClassByFqName(superTypeFqName, memberName) - if (superResult is ResolutionResult.Resolved) { - Log.d(TAG, "resolveMemberInClass: found '$memberName' in supertype '$superTypeFqName'") - return superResult - } - } else { - val superClass = findClassSymbol(simpleName, classSymbol.containingScope) - if (superClass != null) { - val superMember = resolveMemberInClass(superClass, memberName) - if (superMember is ResolutionResult.Resolved) { - return superMember - } - } - } - } - - Log.d(TAG, "resolveMemberInClass: '$memberName' not found in ${classSymbol.qualifiedName}") - return ResolutionResult.Unresolved(memberName) - } - - private fun resolveMemberInClassByFqName(classFqName: String, memberName: String): ResolutionResult { - Log.d(TAG, "resolveMemberInClassByFqName: class='$classFqName', member='$memberName'") - - val classpathIndex = projectIndex?.getClasspathIndex() - if (classpathIndex != null) { - val members = classpathIndex.findMembers(classFqName) - .filter { it.name == memberName } - if (members.isNotEmpty()) { - Log.d(TAG, "resolveMemberInClassByFqName: found ${members.size} members in classpath") - return ResolutionResult.Resolved(members.map { it.toSyntheticSymbol() }) - } - - val indexedClass = classpathIndex.findByFqName(classFqName) - if (indexedClass != null && indexedClass.superTypes.isNotEmpty()) { - Log.d(TAG, "resolveMemberInClassByFqName: searching supertypes: ${indexedClass.superTypes}") - for (superTypeFqName in indexedClass.superTypes) { - val superResult = resolveMemberInClassByFqName(superTypeFqName, memberName) - if (superResult is ResolutionResult.Resolved) { - return superResult - } - } - } - } - - val stdlibMembers = stdlibIndex?.findMembers(classFqName) - ?.filter { it.name == memberName } - if (stdlibMembers != null && stdlibMembers.isNotEmpty()) { - Log.d(TAG, "resolveMemberInClassByFqName: found ${stdlibMembers.size} members in stdlib") - return ResolutionResult.Resolved(stdlibMembers.map { it.toSyntheticSymbol() }) - } - - val stdlibClass = stdlibIndex?.findByFqName(classFqName) - if (stdlibClass != null && stdlibClass.superTypes.isNotEmpty()) { - Log.d(TAG, "resolveMemberInClassByFqName: searching stdlib supertypes: ${stdlibClass.superTypes}") - for (superTypeFqName in stdlibClass.superTypes) { - val superResult = resolveMemberInClassByFqName(superTypeFqName, memberName) - if (superResult is ResolutionResult.Resolved) { - return superResult - } - } - } - - Log.d(TAG, "resolveMemberInClassByFqName: '$memberName' not found in '$classFqName'") - return ResolutionResult.Unresolved(memberName) - } - - /** - * Resolves members on function types (invoke, etc.). - */ - private fun resolveFunctionTypeMember( - type: FunctionType, - memberName: String, - scope: Scope - ): ResolutionResult { - return when (memberName) { - "invoke" -> { - ResolutionResult.Resolved(listOf(createSyntheticInvokeFunction(type))) - } - else -> { - resolveMemberInType(ClassType.ANY, memberName, scope) - } - } - } - - /** - * Resolves extension functions/properties for a receiver type. - * - * @param receiverType The receiver type - * @param name The extension name - * @param scope The scope to search for extensions - * @return List of applicable extension symbols - */ - fun resolveExtension( - receiverType: KotlinType, - name: String, - scope: Scope - ): List { - val candidates = mutableListOf() - - var currentScope: Scope? = scope - while (currentScope != null) { - val localExtensions = currentScope.resolveLocal(name) - .filter { isApplicableExtension(it, receiverType) } - candidates.addAll(localExtensions) - currentScope = currentScope.parent - } - - val importedExtensions = resolveExtensionViaImports(name, receiverType) - candidates.addAll(importedExtensions) - - return candidates.distinctBy { it.id } - } - - /** - * Checks if a symbol is an extension applicable to the receiver type. - */ - private fun isApplicableExtension(symbol: Symbol, receiverType: KotlinType): Boolean { - val extensionReceiverRef = when (symbol) { - is FunctionSymbol -> symbol.receiverType - is PropertySymbol -> symbol.receiverType - else -> return false - } ?: return false - - val extensionReceiverType = context.typeResolver.resolve( - extensionReceiverRef, - symbol.containingScope - ) - - return typeChecker.isSubtypeOf(receiverType, extensionReceiverType) - } - - /** - * Resolves a type name to a symbol. - * - * @param typeName The type name (simple or qualified) - * @param scope The scope for resolution - * @return The resolved class, type alias, or type parameter symbol - */ - fun resolveType(typeName: String, scope: Scope): Symbol? { - val result = if ('.' in typeName) { - resolveQualifiedName(typeName, scope) - } else { - resolveSimpleName(typeName, scope) - } - - return when (result) { - is ResolutionResult.Resolved -> { - result.symbols.firstOrNull { symbol -> - symbol is ClassSymbol || symbol is TypeAliasSymbol || symbol is TypeParameterSymbol - } - } - else -> null - } - } - - /** - * Resolves function overloads for a call with given argument types. - * - * @param name The function name - * @param receiverType Optional receiver type for member/extension calls - * @param argumentTypes Types of the call arguments - * @param scope The scope for resolution - * @return List of applicable function symbols - */ - fun resolveFunctionCall( - name: String, - receiverType: KotlinType?, - argumentTypes: List, - scope: Scope - ): List { - Log.d(TAG, "resolveFunctionCall: name='$name', receiverType=${receiverType?.render()}, argCount=${argumentTypes.size}") - - val candidates = if (receiverType != null) { - Log.d(TAG, "resolveFunctionCall: has receiver, calling resolveMemberAccess") - val memberResult = resolveMemberAccess(receiverType, name, scope) - Log.d(TAG, "resolveFunctionCall: memberResult=$memberResult") - when (memberResult) { - is ResolutionResult.Resolved -> memberResult.symbols.filterIsInstance() - else -> emptyList() - } - } else { - Log.d(TAG, "resolveFunctionCall: no receiver, calling resolveSimpleName") - val result = resolveSimpleName(name, scope) - when (result) { - is ResolutionResult.Resolved -> result.symbols.filterIsInstance() - else -> emptyList() - } - } - - Log.d(TAG, "resolveFunctionCall: got ${candidates.size} raw candidates") - val filtered = candidates.filter { isApplicableFunction(it, argumentTypes) } - Log.d(TAG, "resolveFunctionCall: returning ${filtered.size} applicable candidates") - return filtered - } - - /** - * Checks if a function is applicable with the given argument types. - * Returns true for candidates that might match, allowing for better error messages - * and handling cases where argument types couldn't be fully inferred. - */ - private fun isApplicableFunction( - function: FunctionSymbol, - argumentTypes: List - ): Boolean { - val hasErrorTypes = argumentTypes.any { it is ErrorType || it.hasError } - - if (hasErrorTypes) { - return argumentTypes.size <= function.parameterCount || function.parameters.any { it.isVararg } - } - - if (argumentTypes.isEmpty() && function.parameterCount > 0) { - return true - } - - if (argumentTypes.size < function.requiredParameterCount) { - return false - } - if (argumentTypes.size > function.parameterCount && !function.parameters.any { it.isVararg }) { - return false - } - - for ((index, argType) in argumentTypes.withIndex()) { - val param = function.parameters.getOrNull(index) ?: continue - val paramTypeRef = param.type ?: continue - val paramType = context.typeResolver.resolve(paramTypeRef, function.containingScope) - - if (!typeChecker.isSubtypeOf(argType, paramType)) { - return false - } - } - - return true - } - - /** - * Resolves via explicit imports. - */ - private fun resolveViaImports(name: String): List { - for (import in symbolTable.imports) { - if (import.isStar) { - continue - } - - val importedName = import.alias ?: import.fqName.substringAfterLast('.') - if (importedName == name) { - return resolveImportedFqName(import.fqName) - } - } - - for (import in symbolTable.imports) { - if (!import.isStar) continue - - val fqName = "${import.fqName}.$name" - val resolved = resolveImportedFqName(fqName) - if (resolved.isNotEmpty()) { - return resolved - } - } - - return emptyList() - } - - /** - * Resolves via implicit imports (kotlin.*, etc.). - */ - private fun resolveViaImplicitImports(name: String): List { - for (packageName in implicitImports) { - val fqName = "$packageName.$name" - val resolved = resolveImportedFqName(fqName) - if (resolved.isNotEmpty()) { - return resolved - } - } - return emptyList() - } - - /** - * Resolves via ProjectIndex for cross-file symbol lookup. - * Checks same-package symbols first (visible without import), - * then imported project symbols, then star imports. - */ - private fun resolveViaProjectIndex(name: String): List { - val index = projectIndex ?: return emptyList() - val currentPackage = context.packageName - val currentFilePath = context.filePath - - val samePackageSymbols = index.findByPackage(currentPackage) - .filter { it.name == name && it.filePath != currentFilePath } - .map { it.toSyntheticSymbol() } - if (samePackageSymbols.isNotEmpty()) { - return samePackageSymbols - } - - val importedProjectSymbols = resolveImportedProjectSymbols(name, index) - if (importedProjectSymbols.isNotEmpty()) { - return importedProjectSymbols - } - - for (import in symbolTable.imports.filter { it.isStar }) { - val starImportedSymbols = index.findByPackage(import.fqName) - .filter { it.name == name } - .map { it.toSyntheticSymbol() } - if (starImportedSymbols.isNotEmpty()) { - return starImportedSymbols - } - } - - return emptyList() - } - - /** - * Resolves symbols from imports that reference project files. - */ - private fun resolveImportedProjectSymbols(name: String, index: ProjectIndex): List { - for (import in symbolTable.imports) { - if (import.isStar) { - val packageSymbols = index.findByPackage(import.fqName) - .filter { it.name == name } - .map { it.toSyntheticSymbol() } - if (packageSymbols.isNotEmpty()) { - return packageSymbols - } - } else { - val importedName = import.alias ?: import.fqName.substringAfterLast('.') - if (importedName == name) { - val symbol = index.findByFqName(import.fqName) - if (symbol != null) { - return listOf(symbol.toSyntheticSymbol()) - } - } - } - } - return emptyList() - } - - /** - * Resolves an imported fully qualified name. - * Searches local file scope, StdlibIndex, and ProjectIndex in order. - */ - private fun resolveImportedFqName(fqName: String): List { - val simpleName = fqName.substringAfterLast('.') - - val localResult = symbolTable.fileScope.resolve(simpleName) - .filter { it.qualifiedName == fqName } - if (localResult.isNotEmpty()) { - return localResult - } - - val projectSymbol = projectIndex?.findByFqName(fqName) - if (projectSymbol != null) { - return listOf(projectSymbol.toSyntheticSymbol()) - } - - val stdlibSymbol = stdlibIndex?.findByFqName(fqName) - if (stdlibSymbol != null) { - return listOf(stdlibSymbol.toSyntheticSymbol()) - } - - return emptyList() - } - - /** - * Resolves extensions via imports. - */ - private fun resolveExtensionViaImports(name: String, receiverType: KotlinType): List { - val candidates = mutableListOf() - - for (import in symbolTable.imports) { - if (import.isStar) continue - val importedName = import.alias ?: import.fqName.substringAfterLast('.') - if (importedName != name) continue - - val resolved = resolveImportedFqName(import.fqName) - candidates.addAll(resolved.filter { isApplicableExtension(it, receiverType) }) - } - - return candidates - } - - /** - * Finds a class symbol by fully qualified name. - */ - private fun findClassSymbol(fqName: String, scope: Scope?): ClassSymbol? { - val simpleName = fqName.substringAfterLast('.') - Log.d(TAG, "findClassSymbol: fqName='$fqName', simpleName='$simpleName'") - - val localResult = scope?.resolve(simpleName) - ?.filterIsInstance() - ?.find { it.qualifiedName == fqName || it.name == simpleName } - if (localResult != null) { - Log.d(TAG, "findClassSymbol: found in local scope: ${localResult.qualifiedName}") - return localResult - } - - val fileResult = symbolTable.fileScope.resolve(simpleName) - .filterIsInstance() - .find { it.qualifiedName == fqName || it.name == simpleName } - if (fileResult != null) { - Log.d(TAG, "findClassSymbol: found in file scope: ${fileResult.qualifiedName}") - return fileResult - } - - val projectResult = projectIndex?.findByFqName(fqName) - if (projectResult != null && projectResult.kind.isClass) { - Log.d(TAG, "findClassSymbol: found in project index") - return projectResult.toSyntheticSymbol() as? ClassSymbol - } - - val classpathResult = projectIndex?.getClasspathIndex()?.findByFqName(fqName) - if (classpathResult != null && classpathResult.kind.name.contains("CLASS")) { - Log.d(TAG, "findClassSymbol: found in classpath index") - return classpathResult.toSyntheticSymbol() as? ClassSymbol - } - - val stdlibResult = stdlibIndex?.findByFqName(fqName) - if (stdlibResult != null && stdlibResult.kind.isClass) { - Log.d(TAG, "findClassSymbol: found in stdlib index") - return stdlibResult.toSyntheticSymbol() as? ClassSymbol - } - - Log.d(TAG, "findClassSymbol: not found for '$fqName'") - return null - } - - /** - * Creates a synthetic invoke function for function types. - */ - private fun createSyntheticInvokeFunction(type: FunctionType): FunctionSymbol { - return FunctionSymbol( - name = "invoke", - location = SymbolLocation.SYNTHETIC, - modifiers = Modifiers( - isOperator = true - ), - containingScope = null, - parameters = type.parameterTypes.mapIndexed { index, paramType -> - ParameterSymbol( - name = "p$index", - location = SymbolLocation.SYNTHETIC, - modifiers = Modifiers.EMPTY, - containingScope = null, - type = TypeReference(paramType.render()) - ) - }, - returnType = TypeReference(type.returnType.render()), - receiverType = null - ) - } - - /** - * Resolves 'this' reference in a scope. - * - * @param scope The current scope - * @param label Optional label for labeled 'this' - * @return The 'this' type, or null if not in a valid context - */ - fun resolveThis(scope: Scope, label: String? = null): KotlinType? { - Log.d(TAG, "resolveThis: scope.kind=${scope.kind}, scope.owner=${scope.owner?.name}, label=$label") - - if (label != null) { - var currentScope: Scope? = scope - while (currentScope != null) { - val owner = currentScope.owner - if (owner is ClassSymbol && owner.name == label) { - return ClassType(owner.qualifiedName) - } - if (owner is FunctionSymbol && owner.name == label && owner.receiverType != null) { - return context.typeResolver.resolve(owner.receiverType, currentScope) - } - currentScope = currentScope.parent - } - return null - } - - val callableScope = scope.findEnclosingCallable() - Log.d(TAG, "resolveThis: callableScope=${callableScope?.kind}, callableScope.owner=${callableScope?.owner?.name}") - if (callableScope != null) { - val function = callableScope.owner as? FunctionSymbol - Log.d(TAG, "resolveThis: function=${function?.name}, receiverType=${function?.receiverType?.render()}") - if (function?.receiverType != null) { - return context.typeResolver.resolve(function.receiverType, callableScope) - } - } - - val classScope = scope.findEnclosingClass() - Log.d(TAG, "resolveThis: classScope=${classScope?.kind}, classScope.owner=${classScope?.owner?.name}") - if (classScope != null) { - val classSymbol = classScope.owner as? ClassSymbol - if (classSymbol != null) { - return ClassType(classSymbol.qualifiedName) - } - } - - Log.d(TAG, "resolveThis: returning null") - return null - } - - /** - * Resolves 'super' reference in a scope. - * - * @param scope The current scope - * @param label Optional label for labeled 'super' - * @return The 'super' type, or null if not in a valid context - */ - fun resolveSuper(scope: Scope, label: String? = null): KotlinType? { - val classScope = scope.findEnclosingClass() ?: return null - val classSymbol = classScope.owner as? ClassSymbol ?: return null - - Log.d(TAG, "resolveSuper: class=${classSymbol.name}, superTypes=${classSymbol.superTypes.map { it.render() }}") - - if (label != null) { - for (superTypeRef in classSymbol.superTypes) { - val simpleName = superTypeRef.simpleName - if (simpleName == label) { - val fqNameFromImport = resolveFqNameFromImports(simpleName) - if (fqNameFromImport != null) { - return ClassType(fqNameFromImport) - } - return context.typeResolver.resolve(superTypeRef, classScope) - } - } - return null - } - - val superTypes = classSymbol.superTypes - if (superTypes.isEmpty()) { - Log.d(TAG, "resolveSuper: no superTypes, returning Any") - return ClassType.ANY - } - - for (superTypeRef in superTypes) { - val resolvedSymbol = resolveType(superTypeRef.name, classScope) - Log.d(TAG, "resolveSuper: checking ${superTypeRef.name} -> resolved=$resolvedSymbol, isInterface=${(resolvedSymbol as? ClassSymbol)?.isInterface}") - if (resolvedSymbol is ClassSymbol && !resolvedSymbol.isInterface) { - Log.d(TAG, "resolveSuper: found superclass via resolveType: ${resolvedSymbol.qualifiedName}") - return ClassType(resolvedSymbol.qualifiedName) - } - } - - Log.d(TAG, "resolveSuper: no local superclass found, checking imports/classpath/stdlib for ${superTypes.map { it.render() }}") - - for (superTypeRef in superTypes) { - val simpleName = superTypeRef.simpleName - val fqName = superTypeRef.render() - - val fqNameFromImport = resolveFqNameFromImports(simpleName) - if (fqNameFromImport != null) { - Log.d(TAG, "resolveSuper: resolved via import: $simpleName -> $fqNameFromImport") - return ClassType(fqNameFromImport) - } - - val classpathIndex = projectIndex?.getClasspathIndex() - if (classpathIndex != null) { - val byFqName = classpathIndex.findByFqName(fqName) - if (byFqName != null && byFqName.kind.name.contains("CLASS")) { - Log.d(TAG, "resolveSuper: found in classpath by fqName: $fqName") - return ClassType(fqName) - } - - val bySimpleName = classpathIndex.findBySimpleName(simpleName) - .filter { it.kind.name.contains("CLASS") } - if (bySimpleName.size == 1) { - val found = bySimpleName.first() - Log.d(TAG, "resolveSuper: unique match in classpath by simpleName: ${found.fqName}") - return ClassType(found.fqName) - } - } - - val stdlibIndex = projectIndex?.getStdlibIndex() - if (stdlibIndex != null) { - val byFqName = stdlibIndex.findByFqName(fqName) - if (byFqName != null && byFqName.kind == IndexedSymbolKind.CLASS) { - Log.d(TAG, "resolveSuper: found in stdlib by fqName: $fqName") - return ClassType(fqName) - } - - val bySimpleName = stdlibIndex.findBySimpleName(simpleName) - .filter { it.kind == IndexedSymbolKind.CLASS } - if (bySimpleName.size == 1) { - val found = bySimpleName.first() - Log.d(TAG, "resolveSuper: unique match in stdlib by simpleName: ${found.fqName}") - return ClassType(found.fqName) - } - } - } - - if (superTypes.isNotEmpty()) { - val firstSuperType = superTypes.first() - val simpleName = firstSuperType.simpleName - Log.d(TAG, "resolveSuper: could not fully resolve superType, returning fallback ClassType for '$simpleName'") - return ClassType(simpleName) - } - - Log.d(TAG, "resolveSuper: no superTypes declared, returning null") - return null - } - - private fun resolveFqNameFromImports(simpleName: String): String? { - val imports = context.symbolTable.imports - - for (importInfo in imports) { - if (!importInfo.isStar && importInfo.simpleName == simpleName) { - Log.d(TAG, "resolveFqNameFromImports: $simpleName -> ${importInfo.fqName} (explicit import)") - return importInfo.fqName - } - } - - for (importInfo in imports) { - if (importInfo.isStar) { - val candidateFqName = "${importInfo.fqName}.$simpleName" - val classpathIndex = projectIndex?.getClasspathIndex() - val stdlibIndex = projectIndex?.getStdlibIndex() - - if (classpathIndex?.findByFqName(candidateFqName) != null) { - Log.d(TAG, "resolveFqNameFromImports: $simpleName -> $candidateFqName (star import, found in classpath)") - return candidateFqName - } - if (stdlibIndex?.findByFqName(candidateFqName) != null) { - Log.d(TAG, "resolveFqNameFromImports: $simpleName -> $candidateFqName (star import, found in stdlib)") - return candidateFqName - } - } - } - - return null - } - - /** - * Checks if a type is from an external library (classpath/stdlib) where - * we may not have full method information indexed. - * - * This is used to suppress false positive "unresolved reference" errors - * for member calls on external library types like AppCompatActivity. - */ - fun isExternalLibraryType(type: KotlinType): Boolean { - if (type !is ClassType) return false - - val fqName = type.fqName - if ('.' !in fqName) return false - - val classpathIndex = projectIndex?.getClasspathIndex() - if (classpathIndex != null) { - val classSymbol = classpathIndex.findByFqName(fqName) - if (classSymbol != null) { - return classpathIndex.findMembers(fqName).isEmpty() - } - } - - if (stdlibIndex != null) { - val stdlibSymbol = stdlibIndex.findByFqName(fqName) - if (stdlibSymbol != null) { - return stdlibIndex.findMembers(fqName).isEmpty() - } - } - - return false - } - - /** - * Resolves a reference expression node. - * - * @param node The syntax node representing a reference - * @param scope The current scope - * @return Resolution result - */ - fun resolveReference(node: SyntaxNode, scope: Scope): ResolutionResult { - return when (node.kind) { - SyntaxKind.SIMPLE_IDENTIFIER -> { - val name = node.text - resolveSimpleName(name, scope) - } - SyntaxKind.NAVIGATION_EXPRESSION -> { - resolveNavigationExpression(node, scope) - } - SyntaxKind.CALL_EXPRESSION -> { - resolveCallExpression(node, scope) - } - else -> ResolutionResult.Unresolved(node.text) - } - } - - /** - * Resolves a navigation expression (a.b.c). - */ - private fun resolveNavigationExpression(node: SyntaxNode, scope: Scope): ResolutionResult { - val receiver = node.childByFieldName("receiver") - ?: return ResolutionResult.Unresolved(node.text) - val member = node.childByFieldName("suffix") - ?: node.children.lastOrNull { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - ?: return ResolutionResult.Unresolved(node.text) - - val receiverResult = resolveReference(receiver, scope) - if (receiverResult !is ResolutionResult.Resolved) { - return receiverResult - } - - val memberName = member.text - val receiverSymbol = receiverResult.symbol - - return when (receiverSymbol) { - is ClassSymbol -> { - resolveMemberInClass(receiverSymbol, memberName) - } - is PropertySymbol -> { - val propertyType = receiverSymbol.type?.let { - context.typeResolver.resolve(it, scope) - } ?: return ResolutionResult.Unresolved(memberName) - resolveMemberAccess(propertyType, memberName, scope) - } - is ParameterSymbol -> { - val paramType = receiverSymbol.type?.let { - context.typeResolver.resolve(it, scope) - } ?: return ResolutionResult.Unresolved(memberName) - resolveMemberAccess(paramType, memberName, scope) - } - else -> ResolutionResult.Unresolved(memberName) - } - } - - /** - * Resolves a call expression. - */ - private fun resolveCallExpression(node: SyntaxNode, scope: Scope): ResolutionResult { - val callee = node.childByFieldName("function") - ?: node.children.firstOrNull { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - ?: node.children.firstOrNull { it.kind == SyntaxKind.NAVIGATION_EXPRESSION } - ?: return ResolutionResult.Unresolved(node.text) - - return resolveReference(callee, scope) - } - -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/TypeInferrer.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/TypeInferrer.kt deleted file mode 100644 index a69eeec669..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/semantic/TypeInferrer.kt +++ /dev/null @@ -1,1146 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.semantic - -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxKind -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxNode -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.PropertySymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Scope -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeReference -import org.appdevforall.codeonthego.lsp.kotlin.types.ClassType -import org.appdevforall.codeonthego.lsp.kotlin.types.ErrorType -import org.appdevforall.codeonthego.lsp.kotlin.types.FunctionType -import org.appdevforall.codeonthego.lsp.kotlin.types.KotlinType -import org.appdevforall.codeonthego.lsp.kotlin.types.PrimitiveKind -import org.appdevforall.codeonthego.lsp.kotlin.types.PrimitiveType -import org.appdevforall.codeonthego.lsp.kotlin.types.TypeChecker -import org.appdevforall.codeonthego.lsp.kotlin.types.TypeParameter -import org.appdevforall.codeonthego.lsp.kotlin.types.TypeSubstitution - -/** - * Infers types for expressions in Kotlin source code. - * - * TypeInferrer performs bidirectional type inference: - * - Bottom-up: Infers types from literals and resolved references - * - Top-down: Uses expected type context for lambdas and generic inference - * - * ## Supported Expressions - * - * - Literals: integers, floats, strings, characters, booleans, null - * - References: variables, properties, parameters - * - Function calls: with overload resolution - * - Member access: properties and methods - * - Operators: arithmetic, comparison, logical - * - Control flow: if, when, try-catch - * - Lambdas: with expected type context - * - Object creation: constructor calls - * - Type operations: as, is, as? - * - * ## Usage - * - * ```kotlin - * val inferrer = TypeInferrer(context) - * val type = inferrer.inferType(expressionNode, scope) - * ``` - * - * @property context The analysis context with shared state - */ -class TypeInferrer( - private val context: AnalysisContext -) { - private val typeChecker: TypeChecker = context.typeChecker - private val symbolResolver: SymbolResolver by lazy { SymbolResolver(context) } - private val overloadResolver: OverloadResolver by lazy { OverloadResolver(context) } - - /** - * Infers the type of an expression node. - * - * @param node The expression syntax node - * @param scope The current scope for resolution - * @param expectedType Optional expected type for contextual inference - * @return The inferred type, or ErrorType if inference fails - */ - fun inferType( - node: SyntaxNode, - scope: Scope, - expectedType: KotlinType? = null - ): KotlinType { - val cached = context.getType(node) - if (cached != null) { - return cached - } - - val inferred = inferTypeImpl(node, scope, expectedType) - context.recordType(node, inferred) - return inferred - } - - private fun inferTypeImpl( - node: SyntaxNode, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - return when (node.kind) { - SyntaxKind.INTEGER_LITERAL -> inferIntegerLiteral(node, expectedType) - SyntaxKind.LONG_LITERAL -> PrimitiveType.LONG - SyntaxKind.REAL_LITERAL -> inferRealLiteral(node, expectedType) - SyntaxKind.STRING_LITERAL, - SyntaxKind.LINE_STRING_LITERAL, - SyntaxKind.MULTI_LINE_STRING_LITERAL -> ClassType.STRING - SyntaxKind.CHARACTER_LITERAL -> PrimitiveType.CHAR - SyntaxKind.BOOLEAN_LITERAL -> PrimitiveType.BOOLEAN - SyntaxKind.NULL_LITERAL -> inferNullLiteral(expectedType) - - SyntaxKind.SIMPLE_IDENTIFIER -> inferSimpleIdentifier(node, scope) - SyntaxKind.NAVIGATION_EXPRESSION -> inferNavigationExpression(node, scope) - SyntaxKind.CALL_EXPRESSION -> inferCallExpression(node, scope, expectedType) - SyntaxKind.INDEXING_EXPRESSION -> inferIndexingExpression(node, scope) - - SyntaxKind.PARENTHESIZED_EXPRESSION -> { - val inner = node.namedChildren.firstOrNull() - if (inner != null) inferType(inner, scope, expectedType) - else ErrorType.unresolved("Empty parenthesized expression") - } - - SyntaxKind.PREFIX_EXPRESSION -> inferPrefixExpression(node, scope) - SyntaxKind.POSTFIX_EXPRESSION -> inferPostfixExpression(node, scope) - SyntaxKind.ADDITIVE_EXPRESSION, - SyntaxKind.MULTIPLICATIVE_EXPRESSION -> inferArithmeticExpression(node, scope) - SyntaxKind.COMPARISON_EXPRESSION -> inferBinaryBooleanExpression(node, scope, "comparison") - SyntaxKind.EQUALITY_EXPRESSION -> inferBinaryBooleanExpression(node, scope, "equality") - SyntaxKind.CONJUNCTION_EXPRESSION, - SyntaxKind.DISJUNCTION_EXPRESSION -> inferBinaryBooleanExpression(node, scope, "logical") - SyntaxKind.INFIX_EXPRESSION -> inferInfixExpression(node, scope) - SyntaxKind.ELVIS_EXPRESSION -> inferElvisExpression(node, scope) - SyntaxKind.RANGE_EXPRESSION -> inferRangeExpression(node, scope) - SyntaxKind.AS_EXPRESSION -> inferAsExpression(node, scope) - SyntaxKind.CHECK_EXPRESSION -> PrimitiveType.BOOLEAN - SyntaxKind.SPREAD_EXPRESSION -> inferSpreadExpression(node, scope) - - SyntaxKind.IF_EXPRESSION -> inferIfExpression(node, scope, expectedType) - SyntaxKind.WHEN_EXPRESSION -> inferWhenExpression(node, scope, expectedType) - SyntaxKind.TRY_EXPRESSION -> inferTryExpression(node, scope, expectedType) - - SyntaxKind.LAMBDA_LITERAL -> inferLambdaExpression(node, scope, expectedType) - SyntaxKind.ANONYMOUS_FUNCTION -> inferAnonymousFunction(node, scope) - SyntaxKind.OBJECT_LITERAL -> inferObjectLiteral(node, scope) - - SyntaxKind.THIS_EXPRESSION -> inferThisExpression(node, scope) - SyntaxKind.SUPER_EXPRESSION -> inferSuperExpression(node, scope) - SyntaxKind.JUMP_EXPRESSION -> inferJumpExpression(node, scope) - - SyntaxKind.COLLECTION_LITERAL -> inferCollectionLiteral(node, scope, expectedType) - - SyntaxKind.ASSIGNMENT -> ClassType.UNIT - SyntaxKind.AUGMENTED_ASSIGNMENT -> ClassType.UNIT - - else -> { - val firstChild = node.namedChildren.firstOrNull() - if (firstChild != null) { - inferType(firstChild, scope, expectedType) - } else { - ErrorType.unresolved("Unknown expression: ${node.kind}") - } - } - } - } - - private fun inferIntegerLiteral(node: SyntaxNode, expectedType: KotlinType?): KotlinType { - val text = node.text.replace("_", "") - - if (text.endsWith("L", ignoreCase = true)) { - return PrimitiveType.LONG - } - - return when (expectedType) { - PrimitiveType.BYTE -> PrimitiveType.BYTE - PrimitiveType.SHORT -> PrimitiveType.SHORT - PrimitiveType.LONG -> PrimitiveType.LONG - else -> PrimitiveType.INT - } - } - - private fun inferRealLiteral(node: SyntaxNode, expectedType: KotlinType?): KotlinType { - val text = node.text.replace("_", "") - - if (text.endsWith("f", ignoreCase = true)) { - return PrimitiveType.FLOAT - } - - return when (expectedType) { - PrimitiveType.FLOAT -> PrimitiveType.FLOAT - else -> PrimitiveType.DOUBLE - } - } - - private fun inferNullLiteral(expectedType: KotlinType?): KotlinType { - return expectedType?.nullable() ?: ClassType.NOTHING.nullable() - } - - private fun inferSimpleIdentifier(node: SyntaxNode, scope: Scope): KotlinType { - val name = node.text - val result = symbolResolver.resolveSimpleName(name, scope) - - return when (result) { - is SymbolResolver.ResolutionResult.Resolved -> { - context.recordReference(node, result.symbol) - val baseType = symbolToType(result.symbol, scope) - context.getSmartCastType(result.symbol) ?: baseType - } - is SymbolResolver.ResolutionResult.Unresolved -> { - context.reportError(DiagnosticCode.UNRESOLVED_REFERENCE, node, name) - ErrorType.unresolved(name) - } - is SymbolResolver.ResolutionResult.Ambiguous -> { - ErrorType.unresolved("Ambiguous: $name") - } - } - } - - private fun inferNavigationExpression(node: SyntaxNode, scope: Scope): KotlinType { - val receiver = node.childByFieldName("receiver") - ?: node.namedChildren.firstOrNull() - ?: return ErrorType.unresolved("No receiver in navigation") - - val suffix = node.childByFieldName("suffix") - ?: node.findChild(SyntaxKind.NAVIGATION_SUFFIX)?.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: node.namedChildren.lastOrNull { it.kind == SyntaxKind.SIMPLE_IDENTIFIER && it != receiver } - ?: return ErrorType.unresolved("No suffix in navigation") - - val receiverType = inferType(receiver, scope) - if (receiverType.hasError) { - return receiverType - } - - val navigationOperator = node.children.find { it.kind == SyntaxKind.NAVIGATION_SUFFIX } - ?: node.children.find { it.text == "." || it.text == "?." } - val isSafeCall = navigationOperator?.text == "?." - - val effectiveReceiverType = if (isSafeCall && receiverType.isNullable) { - receiverType.nonNullable() - } else { - receiverType - } - - if (!isSafeCall && receiverType.isNullable) { - context.reportError(DiagnosticCode.UNSAFE_CALL, node, receiverType.render()) - } - - val memberName = suffix.text - val result = symbolResolver.resolveMemberAccess(effectiveReceiverType, memberName, scope) - - return when (result) { - is SymbolResolver.ResolutionResult.Resolved -> { - context.recordReference(suffix, result.symbol) - val memberType = symbolToType(result.symbol, scope) - if (isSafeCall && receiverType.isNullable) memberType.nullable() else memberType - } - is SymbolResolver.ResolutionResult.Unresolved -> { - if (!symbolResolver.isExternalLibraryType(effectiveReceiverType)) { - context.reportError(DiagnosticCode.UNRESOLVED_REFERENCE, suffix, memberName) - } - ErrorType.unresolved(memberName) - } - is SymbolResolver.ResolutionResult.Ambiguous -> { - ErrorType.unresolved("Ambiguous: $memberName") - } - } - } - - private fun inferCallExpression( - node: SyntaxNode, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - val callee = node.childByFieldName("function") - ?: node.namedChildren.firstOrNull { - it.kind == SyntaxKind.SIMPLE_IDENTIFIER || - it.kind == SyntaxKind.NAVIGATION_EXPRESSION - } - ?: return ErrorType.unresolved("No callee in call expression") - - val arguments = node.childByFieldName("arguments") - ?: node.namedChildren.find { it.kind == SyntaxKind.CALL_SUFFIX } - - val argumentTypes = arguments?.namedChildren - ?.filter { it.kind == SyntaxKind.VALUE_ARGUMENT } - ?.map { arg -> - val expr = arg.namedChildren.find { it.kind != SyntaxKind.SIMPLE_IDENTIFIER } - ?: arg.namedChildren.firstOrNull() - if (expr != null) { - inferType(expr, scope) - } else { - ErrorType.UNRESOLVED - } - } - ?: emptyList() - - return when (callee.kind) { - SyntaxKind.SIMPLE_IDENTIFIER -> { - inferSimpleCallExpression(callee, argumentTypes, scope, expectedType) - } - SyntaxKind.NAVIGATION_EXPRESSION -> { - inferMemberCallExpression(callee, argumentTypes, scope, expectedType) - } - else -> { - val calleeType = inferType(callee, scope) - if (calleeType is FunctionType) { - calleeType.returnType - } else { - ErrorType.unresolved("Cannot call ${callee.text}") - } - } - } - } - - private fun inferSimpleCallExpression( - callee: SyntaxNode, - argumentTypes: List, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - val name = callee.text - - val implicitReceiverType = scope.findEnclosingClass()?.let { classScope -> - val classSymbol = classScope.owner as? ClassSymbol - classSymbol?.let { ClassType(it.qualifiedName) } - } - - var candidates = if (implicitReceiverType != null) { - symbolResolver.resolveFunctionCall(name, implicitReceiverType, argumentTypes, scope) - } else { - emptyList() - } - - if (candidates.isEmpty()) { - candidates = symbolResolver.resolveFunctionCall(name, null, argumentTypes, scope) - } - - if (candidates.isEmpty()) { - val resolved = symbolResolver.resolveSimpleName(name, scope) - if (resolved is SymbolResolver.ResolutionResult.Resolved) { - val symbol = resolved.symbol - if (symbol is ParameterSymbol && symbol.type != null) { - val paramType = context.typeResolver.resolve(symbol.type, scope) - if (paramType is FunctionType) { - context.recordReference(callee, symbol) - return paramType.returnType - } - } - if (symbol is PropertySymbol && symbol.type != null) { - val propType = context.typeResolver.resolve(symbol.type, scope) - if (propType is FunctionType) { - context.recordReference(callee, symbol) - return propType.returnType - } - } - if (symbol is ClassSymbol) { - context.recordReference(callee, symbol) - return ClassType(symbol.qualifiedName) - } - } - - context.reportError(DiagnosticCode.UNRESOLVED_REFERENCE, callee, name) - return ErrorType.unresolved(name) - } - - val selected = selectBestOverload(candidates, argumentTypes, expectedType) - context.recordReference(callee, selected) - return resolveReturnType(selected, argumentTypes, scope) - } - - private fun inferMemberCallExpression( - callee: SyntaxNode, - argumentTypes: List, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - val receiver = callee.childByFieldName("receiver") - ?: callee.namedChildren.firstOrNull() - ?: return ErrorType.unresolved("No receiver") - - val suffix = callee.childByFieldName("suffix") - ?: callee.findChild(SyntaxKind.NAVIGATION_SUFFIX)?.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: callee.namedChildren.lastOrNull { it.kind == SyntaxKind.SIMPLE_IDENTIFIER && it != receiver } - ?: return ErrorType.unresolved("No method name") - - val receiverType = inferType(receiver, scope) - if (receiverType.hasError) return receiverType - - val methodName = suffix.text - val candidates = symbolResolver.resolveFunctionCall(methodName, receiverType, argumentTypes, scope) - - if (candidates.isEmpty()) { - if (!symbolResolver.isExternalLibraryType(receiverType)) { - context.reportError(DiagnosticCode.UNRESOLVED_REFERENCE, suffix, methodName) - } - return ErrorType.unresolved(methodName) - } - - val selected = selectBestOverload(candidates, argumentTypes, expectedType) - context.recordReference(suffix, selected) - return resolveReturnType(selected, argumentTypes, scope) - } - - private fun selectBestOverload( - candidates: List, - argumentTypes: List, - expectedType: KotlinType? - ): FunctionSymbol { - if (candidates.size == 1) return candidates.first() - - val callArguments = argumentTypes.map { OverloadResolver.CallArgument(it) } - val result = overloadResolver.resolve(candidates, callArguments, expectedType) - - return when (result) { - is OverloadResolver.OverloadResolutionResult.Success -> result.selected - is OverloadResolver.OverloadResolutionResult.Ambiguity -> result.candidates.first() - is OverloadResolver.OverloadResolutionResult.NoApplicable -> candidates.first() - } - } - - private fun resolveReturnType( - function: FunctionSymbol, - argumentTypes: List, - scope: Scope - ): KotlinType { - val returnTypeRef = function.returnType ?: return ClassType.UNIT - val resolveScope = function.bodyScope ?: function.containingScope ?: scope - val returnType = context.typeResolver.resolve(returnTypeRef, resolveScope) - - if (function.typeParameters.isEmpty()) { - return returnType - } - - val substitution = inferTypeArguments(function, argumentTypes, resolveScope) - return if (substitution.isEmpty) returnType else returnType.substitute(substitution) - } - - private fun inferTypeArguments( - function: FunctionSymbol, - argumentTypes: List, - scope: Scope - ): TypeSubstitution { - val typeParams = function.typeParameters - if (typeParams.isEmpty()) return TypeSubstitution.EMPTY - - val bindings = mutableMapOf() - - function.parameters.forEachIndexed { index, param -> - val argType = argumentTypes.getOrNull(index) ?: return@forEachIndexed - val paramTypeRef = param.type ?: return@forEachIndexed - val paramType = context.typeResolver.resolve(paramTypeRef, function.containingScope ?: scope) - - extractTypeBindings(paramType, argType, typeParams, bindings) - } - - return if (bindings.isEmpty()) TypeSubstitution.EMPTY else TypeSubstitution.of(*bindings.toList().toTypedArray()) - } - - private fun extractTypeBindings( - paramType: KotlinType, - argType: KotlinType, - typeParams: List, - bindings: MutableMap - ) { - when (paramType) { - is TypeParameter -> { - val matching = typeParams.find { it.name == paramType.name } - if (matching != null) { - bindings[paramType] = argType - } - } - is ClassType -> { - if (paramType.typeArguments.isNotEmpty() && argType is ClassType && argType.typeArguments.isNotEmpty()) { - paramType.typeArguments.zip(argType.typeArguments).forEach { (paramArg, argArg) -> - if (paramArg.type != null && argArg.type != null) { - extractTypeBindings(paramArg.type, argArg.type, typeParams, bindings) - } - } - } - } - else -> {} - } - } - - private fun inferIndexingExpression(node: SyntaxNode, scope: Scope): KotlinType { - val receiver = node.namedChildren.firstOrNull() - ?: return ErrorType.unresolved("No receiver in indexing") - - val receiverType = inferType(receiver, scope) - if (receiverType.hasError) return receiverType - - return when { - receiverType is ClassType && receiverType.fqName == "kotlin.Array" -> { - receiverType.typeArguments.firstOrNull()?.type ?: ClassType.ANY - } - receiverType is ClassType && receiverType.fqName.startsWith("kotlin.collections.") -> { - when { - "List" in receiverType.fqName || "Set" in receiverType.fqName -> { - receiverType.typeArguments.firstOrNull()?.type ?: ClassType.ANY - } - "Map" in receiverType.fqName -> { - receiverType.typeArguments.getOrNull(1)?.type?.nullable() ?: ClassType.ANY_NULLABLE - } - else -> ClassType.ANY - } - } - receiverType is ClassType -> { - val getResult = symbolResolver.resolveMemberAccess(receiverType, "get", scope) - if (getResult is SymbolResolver.ResolutionResult.Resolved) { - val getFunction = getResult.symbols.filterIsInstance().firstOrNull() - if (getFunction != null) { - return resolveReturnType(getFunction, emptyList(), scope) - } - } - ClassType.ANY - } - else -> ClassType.ANY - } - } - - private fun inferPrefixExpression(node: SyntaxNode, scope: Scope): KotlinType { - val operator = node.children.firstOrNull { !it.isNamed }?.text - val operand = node.namedChildren.firstOrNull() - ?: return ErrorType.unresolved("No operand in prefix expression") - - val operandType = inferType(operand, scope) - - return when (operator) { - "!", "not" -> PrimitiveType.BOOLEAN - "-", "+" -> operandType - "++", "--" -> operandType - else -> operandType - } - } - - private fun inferPostfixExpression(node: SyntaxNode, scope: Scope): KotlinType { - val operand = node.namedChildren.firstOrNull() - ?: return ErrorType.unresolved("No operand in postfix expression") - - val operator = node.children.lastOrNull { !it.isNamed }?.text - - val operandType = inferType(operand, scope) - - return when (operator) { - "!!" -> operandType.nonNullable() - "++", "--" -> operandType - else -> operandType - } - } - - private fun inferArithmeticExpression(node: SyntaxNode, scope: Scope): KotlinType { - if (node.hasError || node.children.any { it.isError || it.isMissing }) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Incomplete expression '${node.text.trim()}'" - ) - return ErrorType.unresolved("Invalid arithmetic expression") - } - - val children = node.namedChildren - if (children.size < 2) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Incomplete expression '${node.text.trim()}'" - ) - return ErrorType.unresolved("Invalid arithmetic expression") - } - - if (hasInvalidAdjacentTokens(node)) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Invalid expression '${node.text.trim()}'" - ) - return ErrorType.unresolved("Invalid expression") - } - - val leftType = inferType(children.first(), scope) - val rightType = inferType(children.last(), scope) - - if (leftType is ClassType && leftType.fqName == "kotlin.String") { - return ClassType.STRING - } - - return typeChecker.commonSupertype(leftType, rightType) - } - - private fun hasInvalidAdjacentTokens(node: SyntaxNode): Boolean { - val children = node.namedChildren - if (children.size < 2) return false - for (i in 0 until children.size - 1) { - val current = children[i] - val next = children[i + 1] - if (SyntaxKind.isExpression(current.kind) && SyntaxKind.isExpression(next.kind)) { - return true - } - } - return false - } - - private fun inferBinaryBooleanExpression( - node: SyntaxNode, - scope: Scope, - expressionType: String - ): KotlinType { - if (node.hasError || node.children.any { it.isError || it.isMissing }) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Incomplete $expressionType expression '${node.text.trim()}'" - ) - return PrimitiveType.BOOLEAN - } - - val children = node.namedChildren - if (children.size < 2) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Incomplete $expressionType expression '${node.text.trim()}'" - ) - return PrimitiveType.BOOLEAN - } - inferType(children.first(), scope) - inferType(children.last(), scope) - return PrimitiveType.BOOLEAN - } - - private fun inferInfixExpression(node: SyntaxNode, scope: Scope): KotlinType { - if (node.hasError || node.children.any { it.isError || it.isMissing }) { - context.reportError(DiagnosticCode.SYNTAX_ERROR, node, "Incomplete expression '${node.text.trim()}'") - return ErrorType.unresolved("Invalid infix expression") - } - - val children = node.namedChildren - if (children.size < 2) { - context.reportError(DiagnosticCode.SYNTAX_ERROR, node, "Invalid expression '${node.text}'") - return ErrorType.unresolved("Invalid infix expression") - } - - if (hasInvalidAdjacentTokens(node)) { - context.reportError(DiagnosticCode.SYNTAX_ERROR, node, "Invalid expression '${node.text.trim()}'") - return ErrorType.unresolved("Invalid expression") - } - - val leftType = inferType(children.first(), scope) - val operatorNode = node.children.find { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - val operatorName = operatorNode?.text ?: return ErrorType.unresolved("No infix operator") - - val result = symbolResolver.resolveMemberAccess(leftType, operatorName, scope) - return when (result) { - is SymbolResolver.ResolutionResult.Resolved -> { - val function = result.symbols.filterIsInstance().firstOrNull() - if (function != null) { - resolveReturnType(function, emptyList(), scope) - } else { - if (!symbolResolver.isExternalLibraryType(leftType)) { - context.reportError(DiagnosticCode.UNRESOLVED_REFERENCE, operatorNode!!, operatorName) - } - ErrorType.unresolved("Unresolved infix operator: $operatorName") - } - } - else -> { - if (!leftType.hasError && !symbolResolver.isExternalLibraryType(leftType)) { - context.reportError(DiagnosticCode.UNRESOLVED_REFERENCE, operatorNode, operatorName) - } - ErrorType.unresolved("Unresolved infix operator: $operatorName") - } - } - } - - private fun inferElvisExpression(node: SyntaxNode, scope: Scope): KotlinType { - if (node.hasError || node.children.any { it.isError || it.isMissing }) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Incomplete elvis expression '${node.text.trim()}'" - ) - return ErrorType.unresolved("Invalid elvis expression") - } - - val children = node.namedChildren - if (children.size < 2) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Incomplete elvis expression '${node.text.trim()}'" - ) - return ErrorType.unresolved("Invalid elvis expression") - } - - val leftType = inferType(children.first(), scope) - val rightType = inferType(children.last(), scope) - - val leftNonNull = leftType.nonNullable() - return typeChecker.commonSupertype(leftNonNull, rightType) - } - - private fun inferRangeExpression(node: SyntaxNode, scope: Scope): KotlinType { - if (node.hasError || node.children.any { it.isError || it.isMissing }) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Incomplete range expression '${node.text.trim()}'" - ) - return ClassType("kotlin.ranges.IntRange") - } - - val children = node.namedChildren - if (children.size < 2) { - context.reportError( - DiagnosticCode.SYNTAX_ERROR, - node, - "Incomplete range expression '${node.text.trim()}'" - ) - return ClassType("kotlin.ranges.IntRange") - } - - val elementType = inferType(children.first(), scope) - - return when (elementType) { - PrimitiveType.INT -> ClassType("kotlin.ranges.IntRange") - PrimitiveType.LONG -> ClassType("kotlin.ranges.LongRange") - PrimitiveType.CHAR -> ClassType("kotlin.ranges.CharRange") - else -> ClassType("kotlin.ranges.ClosedRange").withArguments(elementType) - } - } - - private fun inferAsExpression(node: SyntaxNode, scope: Scope): KotlinType { - val typeNode = node.namedChildren.lastOrNull { it.kind == SyntaxKind.USER_TYPE } - ?: return ErrorType.unresolved("No target type in as expression") - - val targetTypeRef = parseTypeReference(typeNode) - val targetType = context.typeResolver.resolve(targetTypeRef, scope) - - val operator = node.children.find { it.text == "as?" } - return if (operator != null) { - targetType.nullable() - } else { - targetType - } - } - - private fun inferSpreadExpression(node: SyntaxNode, scope: Scope): KotlinType { - val operand = node.namedChildren.firstOrNull() - ?: return ErrorType.unresolved("No operand in spread expression") - - return inferType(operand, scope) - } - - private fun inferIfExpression( - node: SyntaxNode, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - val thenBranch = node.childByFieldName("consequence") - ?: node.namedChildren.getOrNull(1) - val elseBranch = node.childByFieldName("alternative") - ?: node.namedChildren.getOrNull(2) - - if (thenBranch == null) { - return ClassType.UNIT - } - - val thenType = inferBranchType(thenBranch, scope, expectedType) - - if (elseBranch == null) { - return ClassType.UNIT - } - - val elseType = inferBranchType(elseBranch, scope, expectedType) - - return typeChecker.commonSupertype(thenType, elseType) - } - - private fun inferWhenExpression( - node: SyntaxNode, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - val entries = node.namedChildren.filter { it.kind == SyntaxKind.WHEN_ENTRY } - - if (entries.isEmpty()) { - return ClassType.UNIT - } - - val branchTypes = entries.mapNotNull { entry -> - val body = entry.namedChildren.lastOrNull() - body?.let { inferBranchType(it, scope, expectedType) } - } - - if (branchTypes.isEmpty()) { - return ClassType.UNIT - } - - return typeChecker.commonSupertype(branchTypes) - } - - private fun inferTryExpression( - node: SyntaxNode, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - val tryBlock = node.namedChildren.firstOrNull { it.kind == SyntaxKind.STATEMENTS } - ?: node.namedChildren.firstOrNull() - val catchClauses = node.namedChildren.filter { it.kind == SyntaxKind.CATCH_BLOCK } - val finallyClause = node.namedChildren.find { it.kind == SyntaxKind.FINALLY_BLOCK } - - val tryType = tryBlock?.let { inferBranchType(it, scope, expectedType) } ?: ClassType.UNIT - - if (catchClauses.isEmpty() && finallyClause == null) { - return tryType - } - - val catchTypes = catchClauses.mapNotNull { catch -> - val body = catch.namedChildren.lastOrNull() - body?.let { inferBranchType(it, scope, expectedType) } - } - - if (catchTypes.isEmpty()) { - return tryType - } - - return typeChecker.commonSupertype(listOf(tryType) + catchTypes) - } - - private fun inferBranchType( - branch: SyntaxNode, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - return when (branch.kind) { - SyntaxKind.STATEMENTS -> { - val lastStatement = branch.namedChildren.lastOrNull() - lastStatement?.let { inferType(it, scope, expectedType) } ?: ClassType.UNIT - } - SyntaxKind.CONTROL_STRUCTURE_BODY -> { - val body = branch.namedChildren.firstOrNull() - body?.let { inferBranchType(it, scope, expectedType) } ?: ClassType.UNIT - } - else -> inferType(branch, scope, expectedType) - } - } - - private fun inferLambdaExpression( - node: SyntaxNode, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - val parameters = node.namedChildren.find { it.kind == SyntaxKind.LAMBDA_PARAMETERS } - val body = node.namedChildren.find { it.kind == SyntaxKind.STATEMENTS } - ?: node.namedChildren.lastOrNull() - - val expectedFunctionType = expectedType as? FunctionType - - val paramTypes = if (parameters != null) { - parameters.namedChildren.mapIndexed { index, param -> - val typeAnnotation = param.namedChildren.find { it.kind == SyntaxKind.USER_TYPE } - if (typeAnnotation != null) { - context.typeResolver.resolve(parseTypeReference(typeAnnotation), scope) - } else { - expectedFunctionType?.parameterTypes?.getOrNull(index) ?: ClassType.ANY - } - } - } else if (expectedFunctionType != null && expectedFunctionType.arity == 1) { - listOf(expectedFunctionType.parameterTypes.first()) - } else { - emptyList() - } - - val returnType = if (body != null) { - inferBranchType(body, scope, expectedFunctionType?.returnType) - } else { - ClassType.UNIT - } - - return FunctionType( - parameterTypes = paramTypes, - returnType = returnType, - receiverType = expectedFunctionType?.receiverType - ) - } - - private fun inferAnonymousFunction(node: SyntaxNode, scope: Scope): KotlinType { - val parameters = node.namedChildren.filter { it.kind == SyntaxKind.PARAMETER } - val returnTypeNode = node.namedChildren.find { it.kind == SyntaxKind.USER_TYPE } - - val paramTypes = parameters.map { param -> - val typeAnnotation = param.namedChildren.find { it.kind == SyntaxKind.USER_TYPE } - if (typeAnnotation != null) { - context.typeResolver.resolve(parseTypeReference(typeAnnotation), scope) - } else { - ClassType.ANY - } - } - - val returnType = if (returnTypeNode != null) { - context.typeResolver.resolve(parseTypeReference(returnTypeNode), scope) - } else { - ClassType.UNIT - } - - return FunctionType(parameterTypes = paramTypes, returnType = returnType) - } - - private fun inferObjectLiteral(node: SyntaxNode, scope: Scope): KotlinType { - val superTypes = node.namedChildren.find { it.kind == SyntaxKind.DELEGATION_SPECIFIERS } - val firstSuperType = superTypes?.namedChildren?.firstOrNull() - - if (firstSuperType != null) { - val typeNode = firstSuperType.namedChildren.find { it.kind == SyntaxKind.USER_TYPE } - if (typeNode != null) { - return context.typeResolver.resolve(parseTypeReference(typeNode), scope) - } - } - - return ClassType.ANY - } - - private fun inferCallableReference(node: SyntaxNode, scope: Scope): KotlinType { - val receiverNode = node.namedChildren.firstOrNull() - val member = node.namedChildren.lastOrNull { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - ?: return ErrorType.unresolved("No member in callable reference") - - val memberName = member.text - - if (receiverNode != null && receiverNode.kind == SyntaxKind.USER_TYPE) { - val receiverType = context.typeResolver.resolve(parseTypeReference(receiverNode), scope) - val result = symbolResolver.resolveMemberAccess(receiverType, memberName, scope) - - return when (result) { - is SymbolResolver.ResolutionResult.Resolved -> { - when (val symbol = result.symbol) { - is FunctionSymbol -> symbolToFunctionType(symbol, scope) - is PropertySymbol -> { - val propType = symbol.type?.let { - context.typeResolver.resolve(it, scope) - } ?: ClassType.ANY - ClassType("kotlin.reflect.KProperty1").withArguments(receiverType, propType) - } - else -> ErrorType.unresolved(memberName) - } - } - else -> ErrorType.unresolved(memberName) - } - } - - val result = symbolResolver.resolveSimpleName(memberName, scope) - return when (result) { - is SymbolResolver.ResolutionResult.Resolved -> { - when (val symbol = result.symbol) { - is FunctionSymbol -> symbolToFunctionType(symbol, scope) - is PropertySymbol -> { - val propType = symbol.type?.let { - context.typeResolver.resolve(it, scope) - } ?: ClassType.ANY - ClassType("kotlin.reflect.KProperty0").withArguments(propType) - } - else -> ErrorType.unresolved(memberName) - } - } - else -> ErrorType.unresolved(memberName) - } - } - - private fun inferThisExpression(node: SyntaxNode, scope: Scope): KotlinType { - val label = node.namedChildren - .find { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - ?.text - - return symbolResolver.resolveThis(scope, label) - ?: run { - context.reportError(DiagnosticCode.THIS_NOT_AVAILABLE, node) - ErrorType.unresolved("this") - } - } - - private fun inferSuperExpression(node: SyntaxNode, scope: Scope): KotlinType { - val label = node.namedChildren - .find { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - ?.text - - return symbolResolver.resolveSuper(scope, label) - ?: run { - context.reportError(DiagnosticCode.SUPER_NOT_AVAILABLE, node) - ErrorType.unresolved("super") - } - } - - private fun inferJumpExpression(node: SyntaxNode, scope: Scope): KotlinType { - val keyword = node.children.firstOrNull { !it.isNamed }?.text - return when (keyword) { - "return" -> { - val value = node.namedChildren.firstOrNull() - if (value != null) { - inferType(value, scope) - } - ClassType.NOTHING - } - "throw" -> ClassType.NOTHING - "break", "continue" -> ClassType.NOTHING - else -> ClassType.NOTHING - } - } - - private fun inferCollectionLiteral( - node: SyntaxNode, - scope: Scope, - expectedType: KotlinType? - ): KotlinType { - val elements = node.namedChildren - - if (elements.isEmpty()) { - return when { - expectedType is ClassType && "List" in expectedType.fqName -> expectedType - expectedType is ClassType && "Array" in expectedType.fqName -> expectedType - else -> ClassType.LIST.withArguments(ClassType.NOTHING) - } - } - - val elementTypes = elements.map { inferType(it, scope) } - val elementType = typeChecker.commonSupertype(elementTypes) - - return ClassType.ARRAY.withArguments(elementType) - } - - private fun symbolToType(symbol: Symbol, scope: Scope): KotlinType { - context.getSymbolType(symbol)?.let { return it } - - return when (symbol) { - is PropertySymbol -> { - symbol.type?.let { context.typeResolver.resolve(it, scope) } - ?: inferPropertyTypeFromInitializer(symbol, scope) - } - is ParameterSymbol -> { - val baseType = symbol.type?.let { context.typeResolver.resolve(it, scope) } - ?: return ErrorType.unresolved(symbol.name) - if (symbol.isVararg) { - varargToArrayType(baseType) - } else { - baseType - } - } - is FunctionSymbol -> symbolToFunctionType(symbol, scope) - is ClassSymbol -> ClassType(symbol.qualifiedName) - else -> ErrorType.unresolved(symbol.name) - } - } - - private fun inferPropertyTypeFromInitializer(symbol: PropertySymbol, scope: Scope): KotlinType { - if (!symbol.hasInitializer) { - return ErrorType.unresolved(symbol.name) - } - - if (!context.startComputingType(symbol)) { - return ErrorType.unresolved("Circular: ${symbol.name}") - } - - try { - val propertyNode = context.tree.nodeAtPosition(symbol.location.startPosition) - ?: return ErrorType.unresolved(symbol.name) - - val declNode = if (propertyNode.kind == SyntaxKind.PROPERTY_DECLARATION) { - propertyNode - } else { - propertyNode.parent?.takeIf { it.kind == SyntaxKind.PROPERTY_DECLARATION } - ?: findAncestor(propertyNode, SyntaxKind.PROPERTY_DECLARATION) - } - ?: return ErrorType.unresolved(symbol.name) - - val initializer = declNode.childByFieldName("value") - ?: return ErrorType.unresolved(symbol.name) - - val inferredType = inferType(initializer, symbol.containingScope ?: scope) - context.recordSymbolType(symbol, inferredType) - return inferredType - } finally { - context.finishComputingType(symbol) - } - } - - private fun findAncestor(node: SyntaxNode, kind: SyntaxKind): SyntaxNode? { - var current = node.parent - while (current != null) { - if (current.kind == kind) return current - current = current.parent - } - return null - } - - private fun varargToArrayType(elementType: KotlinType): KotlinType { - return when (elementType) { - is PrimitiveType -> when (elementType.kind) { - PrimitiveKind.INT -> ClassType("kotlin.IntArray") - PrimitiveKind.LONG -> ClassType("kotlin.LongArray") - PrimitiveKind.SHORT -> ClassType("kotlin.ShortArray") - PrimitiveKind.BYTE -> ClassType("kotlin.ByteArray") - PrimitiveKind.FLOAT -> ClassType("kotlin.FloatArray") - PrimitiveKind.DOUBLE -> ClassType("kotlin.DoubleArray") - PrimitiveKind.CHAR -> ClassType("kotlin.CharArray") - PrimitiveKind.BOOLEAN -> ClassType("kotlin.BooleanArray") - else -> ClassType.ARRAY.withArguments(elementType) - } - is ClassType -> when (elementType.fqName) { - "kotlin.Int" -> ClassType("kotlin.IntArray") - "kotlin.Long" -> ClassType("kotlin.LongArray") - "kotlin.Short" -> ClassType("kotlin.ShortArray") - "kotlin.Byte" -> ClassType("kotlin.ByteArray") - "kotlin.Float" -> ClassType("kotlin.FloatArray") - "kotlin.Double" -> ClassType("kotlin.DoubleArray") - "kotlin.Char" -> ClassType("kotlin.CharArray") - "kotlin.Boolean" -> ClassType("kotlin.BooleanArray") - else -> ClassType.ARRAY.withArguments(elementType) - } - else -> ClassType.ARRAY.withArguments(elementType) - } - } - - private fun symbolToFunctionType(function: FunctionSymbol, scope: Scope): FunctionType { - val paramTypes = function.parameters.map { param -> - param.type?.let { context.typeResolver.resolve(it, scope) } ?: ClassType.ANY - } - - val returnType = function.returnType?.let { - context.typeResolver.resolve(it, function.containingScope ?: scope) - } ?: ClassType.UNIT - - val receiverType = function.receiverType?.let { - context.typeResolver.resolve(it, function.containingScope ?: scope) - } - - return FunctionType( - parameterTypes = paramTypes, - returnType = returnType, - receiverType = receiverType, - isSuspend = function.isSuspend - ) - } - - private fun parseTypeReference(node: SyntaxNode): TypeReference { - val nameNode = node.namedChildren.find { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - ?: node.namedChildren.firstOrNull() - - val name = nameNode?.text ?: node.text - - val typeArgs = node.namedChildren - .find { it.kind == SyntaxKind.TYPE_ARGUMENTS } - ?.namedChildren - ?.filter { it.kind == SyntaxKind.TYPE_PROJECTION } - ?.mapNotNull { projection -> - projection.namedChildren.find { it.kind == SyntaxKind.USER_TYPE } - } - ?.map { parseTypeReference(it) } - ?: emptyList() - - val isNullable = node.parent?.children?.any { it.text == "?" } == true - - return TypeReference(name, typeArgs, isNullable, node.range) - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisCache.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisCache.kt deleted file mode 100644 index 61c7e67150..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisCache.kt +++ /dev/null @@ -1,123 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server - -import org.appdevforall.codeonthego.lsp.kotlin.parser.ParseResult -import org.appdevforall.codeonthego.lsp.kotlin.semantic.AnalysisContext -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable -import java.security.MessageDigest -import java.util.concurrent.ConcurrentHashMap - -class AnalysisCache( - private val maxCacheSize: Int = 100 -) { - private val parseCache = ConcurrentHashMap() - private val symbolTableCache = ConcurrentHashMap() - private val analysisCache = ConcurrentHashMap() - - fun getCachedParse(uri: String, content: String): ParseResult? { - val hash = computeHash(content) - val cached = parseCache[uri] ?: return null - return if (cached.contentHash == hash) cached.result else null - } - - fun cacheParse(uri: String, content: String, result: ParseResult) { - evictIfNeeded(parseCache) - parseCache[uri] = CachedParseResult( - contentHash = computeHash(content), - result = result, - timestamp = System.currentTimeMillis() - ) - } - - fun getCachedSymbolTable(uri: String, content: String): SymbolTable? { - val hash = computeHash(content) - val cached = symbolTableCache[uri] ?: return null - return if (cached.contentHash == hash) cached.symbolTable else null - } - - fun cacheSymbolTable(uri: String, content: String, symbolTable: SymbolTable) { - evictIfNeeded(symbolTableCache) - symbolTableCache[uri] = CachedSymbolTable( - contentHash = computeHash(content), - symbolTable = symbolTable, - timestamp = System.currentTimeMillis() - ) - } - - fun getCachedAnalysis(uri: String, content: String): AnalysisContext? { - val hash = computeHash(content) - val cached = analysisCache[uri] ?: return null - return if (cached.contentHash == hash) cached.context else null - } - - fun cacheAnalysis(uri: String, content: String, context: AnalysisContext) { - evictIfNeeded(analysisCache) - analysisCache[uri] = CachedAnalysis( - contentHash = computeHash(content), - context = context, - timestamp = System.currentTimeMillis() - ) - } - - fun invalidate(uri: String) { - parseCache.remove(uri) - symbolTableCache.remove(uri) - analysisCache.remove(uri) - } - - fun invalidateAll() { - parseCache.clear() - symbolTableCache.clear() - analysisCache.clear() - } - - fun cacheStats(): CacheStats { - return CacheStats( - parseEntries = parseCache.size, - symbolTableEntries = symbolTableCache.size, - analysisEntries = analysisCache.size - ) - } - - private fun computeHash(content: String): String { - val digest = MessageDigest.getInstance("MD5") - val hashBytes = digest.digest(content.toByteArray()) - return hashBytes.joinToString("") { "%02x".format(it) } - } - - private fun evictIfNeeded(cache: ConcurrentHashMap) { - if (cache.size >= maxCacheSize) { - val oldestEntry = cache.entries - .minByOrNull { it.value.timestamp } - ?.key - oldestEntry?.let { cache.remove(it) } - } - } -} - -interface Timestamped { - val timestamp: Long -} - -data class CachedParseResult( - val contentHash: String, - val result: ParseResult, - override val timestamp: Long -) : Timestamped - -data class CachedSymbolTable( - val contentHash: String, - val symbolTable: SymbolTable, - override val timestamp: Long -) : Timestamped - -data class CachedAnalysis( - val contentHash: String, - val context: AnalysisContext, - override val timestamp: Long -) : Timestamped - -data class CacheStats( - val parseEntries: Int, - val symbolTableEntries: Int, - val analysisEntries: Int -) diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisScheduler.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisScheduler.kt deleted file mode 100644 index a7e87777a0..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/AnalysisScheduler.kt +++ /dev/null @@ -1,326 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import org.appdevforall.codeonthego.lsp.kotlin.index.FileIndex -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.parser.KotlinParser -import org.appdevforall.codeonthego.lsp.kotlin.parser.ParseResult -import org.appdevforall.codeonthego.lsp.kotlin.semantic.AnalysisContext -import org.appdevforall.codeonthego.lsp.kotlin.semantic.Diagnostic -import org.appdevforall.codeonthego.lsp.kotlin.semantic.DiagnosticCode -import org.appdevforall.codeonthego.lsp.kotlin.semantic.DiagnosticSeverity -import org.appdevforall.codeonthego.lsp.kotlin.semantic.SemanticAnalyzer -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolBuilder -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable -import android.util.Log -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors - -private const val TAG = "AnalysisScheduler" - -/** - * Coordinates background analysis of documents. - * - * AnalysisScheduler manages the parsing and semantic analysis pipeline, - * debouncing rapid edits and publishing diagnostics when analysis completes. - * - * ## Architecture - * - * ``` - * Document Edit - * │ - * ▼ - * ┌─────────────┐ - * │ Debounce │ (100ms delay for rapid typing) - * └─────────────┘ - * │ - * ▼ - * ┌─────────────┐ - * │ Parse │ (tree-sitter, fast) - * └─────────────┘ - * │ - * ▼ - * ┌─────────────┐ - * │ Symbols │ (extract declarations) - * └─────────────┘ - * │ - * ▼ - * ┌─────────────┐ - * │ Semantic │ (type checking, resolution) - * └─────────────┘ - * │ - * ▼ - * ┌─────────────┐ - * │ Diagnostics │ (publish to client) - * └─────────────┘ - * ``` - * - * ## Thread Safety - * - * All operations are thread-safe. Analysis runs on a dedicated - * coroutine dispatcher to avoid blocking the main LSP thread. - * - * @param documentManager Source of document content - * @param projectIndex Project symbol index to update - * @param debounceMs Delay before processing edits (default 100ms) - */ -class AnalysisScheduler( - private val documentManager: DocumentManager, - private val projectIndex: ProjectIndex, - private val debounceMs: Long = 500L -) { - private val analysisExecutor = Executors.newSingleThreadExecutor { runnable -> - Thread(runnable, "ktlsp-analysis-thread").apply { - isDaemon = true - } - } - private val analysisDispatcher = analysisExecutor.asCoroutineDispatcher() - private val scope = CoroutineScope(SupervisorJob() + analysisDispatcher) - - private val parserLock = Any() - @Volatile - private var _parser: KotlinParser? = null - - private fun getParser(): KotlinParser { - _parser?.let { return it } - synchronized(parserLock) { - _parser?.let { return it } - return KotlinParser().also { _parser = it } - } - } - - private val analysisRequests = Channel(Channel.CONFLATED) - private val pendingAnalysis = ConcurrentHashMap() - - private val _diagnosticsFlow = MutableSharedFlow( - replay = 0, - extraBufferCapacity = 64 - ) - - val diagnosticsFlow: SharedFlow = _diagnosticsFlow.asSharedFlow() - - @Volatile - private var isRunning = false - - @Volatile - private var diagnosticsListener: DiagnosticsListener? = null - - fun start() { - if (isRunning) return - isRunning = true - - scope.launch { - for (request in analysisRequests) { - processRequest(request) - } - } - } - - fun stop() { - isRunning = false - pendingAnalysis.values.forEach { it.cancel() } - pendingAnalysis.clear() - scope.cancel() - _parser?.close() - _parser = null - analysisExecutor.shutdown() - } - - fun setDiagnosticsListener(listener: DiagnosticsListener?) { - diagnosticsListener = listener - } - - fun scheduleAnalysis(uri: String, priority: AnalysisPriority = AnalysisPriority.NORMAL) { - if (!isRunning) return - - pendingAnalysis[uri]?.cancel() - - val job = scope.launch { - if (debounceMs > 0 && priority != AnalysisPriority.IMMEDIATE) { - delay(debounceMs) - } - analysisRequests.send(AnalysisRequest(uri, priority)) - } - - pendingAnalysis[uri] = job - } - - fun scheduleImmediateAnalysis(uri: String) { - scheduleAnalysis(uri, AnalysisPriority.IMMEDIATE) - } - - fun analyzeSync(uri: String): AnalysisResult? { - val state = documentManager.get(uri) ?: return null - return runBlocking(analysisDispatcher) { - performAnalysis(state) - } - } - - suspend fun ensureAnalyzed(uri: String): AnalysisResult? { - val state = documentManager.get(uri) ?: return null - - if (state.isAnalyzed) { - return AnalysisResult( - uri = uri, - version = state.version, - parseResult = state.parseResult, - symbolTable = state.symbolTable, - analysisContext = state.analysisContext, - fileIndex = state.fileIndex, - diagnostics = state.diagnostics, - analysisTimeMs = 0 - ) - } - - return withContext(analysisDispatcher) { - performAnalysis(state) - } - } - - fun cancelAnalysis(uri: String) { - pendingAnalysis.remove(uri)?.cancel() - } - - fun cancelAll() { - pendingAnalysis.values.forEach { it.cancel() } - pendingAnalysis.clear() - } - - fun isPending(uri: String): Boolean { - return pendingAnalysis[uri]?.isActive == true - } - - fun pendingCount(): Int { - return pendingAnalysis.count { it.value.isActive } - } - - private suspend fun processRequest(request: AnalysisRequest) { - val state = documentManager.get(request.uri) ?: return - - val result = performAnalysis(state) - - val update = DiagnosticsUpdate( - uri = request.uri, - version = state.version, - diagnostics = result.diagnostics - ) - - _diagnosticsFlow.emit(update) - diagnosticsListener?.onDiagnostics(update) - - pendingAnalysis.remove(request.uri) - } - - private fun performAnalysis(state: DocumentState): AnalysisResult { - val startTime = System.currentTimeMillis() - - - val parseResult = getParser().parse(state.content) - state.setParsed(parseResult) - - for (err in parseResult.syntaxErrors) { - Log.d(TAG, "[ANALYSIS] SyntaxError: '${err.message}', range=${err.range}, text='${err.errorNodeText}'") - } - - val symbolTable = SymbolBuilder.build(parseResult.tree, state.filePath) - - val syntaxErrorRanges = parseResult.syntaxErrors.map { it.range } - - val analysisContext = AnalysisContext( - tree = parseResult.tree, - symbolTable = symbolTable, - filePath = state.filePath, - stdlibIndex = projectIndex.getStdlibIndex(), - projectIndex = projectIndex, - syntaxErrorRanges = syntaxErrorRanges - ) - - for (syntaxError in parseResult.syntaxErrors) { - analysisContext.diagnostics.error( - DiagnosticCode.SYNTAX_ERROR, - syntaxError.range, - syntaxError.message, - filePath = state.filePath - ) - } - - val semanticAnalyzer = SemanticAnalyzer(analysisContext) - try { - semanticAnalyzer.analyze() - } catch (e: Exception) { - Log.e(TAG, "Semantic analysis failed: ${e.message}", e) - } - - val allDiagnostics = analysisContext.diagnostics.diagnostics - val diagnostics = allDiagnostics.filter { diagnostic -> - if (diagnostic.code == DiagnosticCode.SYNTAX_ERROR) { - true - } else { - !syntaxErrorRanges.any { errorRange -> diagnostic.range.overlaps(errorRange) } - } - } - Log.d(TAG, "Diagnostics generated: ${diagnostics.size} (filtered from ${allDiagnostics.size}, syntax errors: ${parseResult.syntaxErrors.size})") - for (diag in diagnostics.take(10)) { - Log.d(TAG, " Diagnostic: code=${diag.code}, severity=${diag.severity}, message='${diag.message.take(60)}', range=${diag.range}") - } - - val fileIndex = FileIndex.fromSymbolTable(symbolTable) - - state.setAnalyzed(symbolTable, analysisContext, fileIndex, diagnostics) - - projectIndex.updateFile(fileIndex) - - val elapsedMs = System.currentTimeMillis() - startTime - - return AnalysisResult( - uri = state.uri, - version = state.version, - parseResult = parseResult, - symbolTable = symbolTable, - analysisContext = analysisContext, - fileIndex = fileIndex, - diagnostics = diagnostics, - analysisTimeMs = elapsedMs - ) - } -} - -enum class AnalysisPriority { - LOW, - NORMAL, - HIGH, - IMMEDIATE -} - -private data class AnalysisRequest( - val uri: String, - val priority: AnalysisPriority -) - -data class AnalysisResult( - val uri: String, - val version: Int, - val parseResult: ParseResult?, - val symbolTable: SymbolTable?, - val analysisContext: AnalysisContext?, - val fileIndex: FileIndex?, - val diagnostics: List, - val analysisTimeMs: Long -) { - val isSuccessful: Boolean get() = parseResult != null && symbolTable != null - val hasErrors: Boolean get() = diagnostics.any { - it.severity == DiagnosticSeverity.ERROR - } -} - -data class DiagnosticsUpdate( - val uri: String, - val version: Int, - val diagnostics: List -) - -fun interface DiagnosticsListener { - fun onDiagnostics(update: DiagnosticsUpdate) -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentManager.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentManager.kt deleted file mode 100644 index f08c3d1659..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentManager.kt +++ /dev/null @@ -1,264 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server - -import java.util.concurrent.ConcurrentHashMap - -/** - * Manages open documents and their analysis state. - * - * DocumentManager tracks all documents currently open in the editor, - * providing efficient lookup and update operations. - * - * ## Thread Safety - * - * All operations are thread-safe. Document states can be accessed - * concurrently from multiple analysis threads. - * - * ## Usage - * - * ```kotlin - * val manager = DocumentManager() - * - * // Open a document - * manager.open("file:///src/Main.kt", "fun main() {}", 1) - * - * // Update content - * manager.update("file:///src/Main.kt", "fun main() { println() }", 2) - * - * // Get state - * val state = manager.get("file:///src/Main.kt") - * - * // Close document - * manager.close("file:///src/Main.kt") - * ``` - */ -class DocumentManager { - - private val documents = ConcurrentHashMap() - - val openDocumentCount: Int get() = documents.size - - val openDocuments: Collection get() = documents.values.toList() - - val openUris: Set get() = documents.keys.toSet() - - /** - * Opens a new document. - * - * @param uri Document URI - * @param content Initial content - * @param version Initial version - * @return The created document state - */ - fun open(uri: String, content: String, version: Int): DocumentState { - val state = DocumentState(uri, version, content) - documents[uri] = state - return state - } - - /** - * Closes a document. - * - * @param uri Document URI - * @return The closed document state, or null if not found - */ - fun close(uri: String): DocumentState? { - return documents.remove(uri) - } - - /** - * Gets a document by URI. - * - * @param uri Document URI - * @return The document state, or null if not open - */ - fun get(uri: String): DocumentState? { - return documents[uri] - } - - /** - * Gets a document by URI, throwing if not found. - */ - fun getOrThrow(uri: String): DocumentState { - return documents[uri] ?: throw DocumentNotFoundException(uri) - } - - /** - * Checks if a document is open. - */ - fun isOpen(uri: String): Boolean { - return documents.containsKey(uri) - } - - /** - * Updates document content (full sync). - * - * @param uri Document URI - * @param content New content - * @param version New version - * @return The updated document state - */ - fun update(uri: String, content: String, version: Int): DocumentState { - val state = documents[uri] ?: throw DocumentNotFoundException(uri) - state.updateContent(content, version) - return state - } - - /** - * Applies an incremental change to a document. - * - * @param uri Document URI - * @param startLine Start line (0-based) - * @param startChar Start character (0-based) - * @param endLine End line (0-based) - * @param endChar End character (0-based) - * @param newText Replacement text - * @param version New version - * @return The updated document state - */ - fun applyChange( - uri: String, - startLine: Int, - startChar: Int, - endLine: Int, - endChar: Int, - newText: String, - version: Int - ): DocumentState { - val state = documents[uri] ?: throw DocumentNotFoundException(uri) - state.applyChange(startLine, startChar, endLine, endChar, newText, version) - return state - } - - /** - * Applies an incremental change using direct character indices. - * This bypasses line/column to offset conversion for more reliable sync. - * - * @param uri Document URI - * @param startIndex Start character index (0-based) - * @param endIndex End character index (0-based) - * @param newText Replacement text - * @param version New version - * @return The updated document state - */ - fun applyChangeByIndex( - uri: String, - startIndex: Int, - endIndex: Int, - newText: String, - version: Int - ): DocumentState { - val state = documents[uri] ?: throw DocumentNotFoundException(uri) - state.applyChangeByIndex(startIndex, endIndex, newText, version) - return state - } - - /** - * Gets all documents that need parsing. - */ - fun getUnparsedDocuments(): List { - return documents.values.filter { !it.isParsed } - } - - /** - * Gets all documents that need analysis. - */ - fun getUnanalyzedDocuments(): List { - return documents.values.filter { it.isParsed && !it.isAnalyzed } - } - - /** - * Gets all documents in a package. - */ - fun getDocumentsInPackage(packageName: String): List { - return documents.values.filter { it.packageName == packageName } - } - - /** - * Gets all documents with errors. - */ - fun getDocumentsWithErrors(): List { - return documents.values.filter { it.hasErrors } - } - - /** - * Gets all analyzed documents. - */ - fun getAnalyzedDocuments(): List { - return documents.values.filter { it.isAnalyzed } - } - - /** - * Finds document by file path. - */ - fun findByPath(path: String): DocumentState? { - return documents.values.find { it.filePath == path } - } - - /** - * Gets the document containing a position. - */ - fun getAtPosition(uri: String, line: Int, character: Int): DocumentState? { - val state = documents[uri] ?: return null - if (line < 0 || line >= state.lineCount) return null - return state - } - - /** - * Invalidates all documents (forces re-analysis). - */ - fun invalidateAll() { - documents.values.forEach { it.invalidate() } - } - - /** - * Invalidates documents in a package. - */ - fun invalidatePackage(packageName: String) { - documents.values - .filter { it.packageName == packageName } - .forEach { it.invalidate() } - } - - /** - * Clears all documents. - */ - fun clear() { - documents.clear() - } - - /** - * Gets statistics about open documents. - */ - fun getStatistics(): DocumentStatistics { - val docs = documents.values.toList() - return DocumentStatistics( - totalDocuments = docs.size, - parsedDocuments = docs.count { it.isParsed }, - analyzedDocuments = docs.count { it.isAnalyzed }, - documentsWithErrors = docs.count { it.hasErrors }, - totalLines = docs.sumOf { it.lineCount }, - totalCharacters = docs.sumOf { it.content.length } - ) - } - - override fun toString(): String { - return "DocumentManager(documents=${documents.size})" - } -} - -/** - * Statistics about open documents. - */ -data class DocumentStatistics( - val totalDocuments: Int, - val parsedDocuments: Int, - val analyzedDocuments: Int, - val documentsWithErrors: Int, - val totalLines: Int, - val totalCharacters: Int -) - -/** - * Exception thrown when a document is not found. - */ -class DocumentNotFoundException(uri: String) : Exception("Document not found: $uri") diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentState.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentState.kt deleted file mode 100644 index b7a41e5020..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/DocumentState.kt +++ /dev/null @@ -1,302 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server - -import android.util.Log -import org.appdevforall.codeonthego.lsp.kotlin.index.FileIndex -import org.appdevforall.codeonthego.lsp.kotlin.parser.ParseResult -import org.appdevforall.codeonthego.lsp.kotlin.parser.PositionConverter -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxTree -import org.appdevforall.codeonthego.lsp.kotlin.semantic.AnalysisContext -import org.appdevforall.codeonthego.lsp.kotlin.semantic.Diagnostic -import org.appdevforall.codeonthego.lsp.kotlin.semantic.DiagnosticSeverity -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable - -private const val TAG = "DocumentState" - -/** - * Per-document analysis state. - * - * DocumentState holds all analysis artifacts for a single open document, - * including the parse tree, symbol table, diagnostics, and file index. - * - * ## Lifecycle - * - * ``` - * EMPTY -> PARSED -> ANALYZED - * ^ | - * +-------------------+ - * (on edit) - * ``` - * - * @property uri Document URI - * @property version Document version (increments on each edit) - * @property content Current document text - */ -class DocumentState( - val uri: String, - var version: Int = 0, - var content: String = "" -) { - var parseResult: ParseResult? = null - private set - - var syntaxTree: SyntaxTree? = null - private set - - var symbolTable: SymbolTable? = null - private set - - var analysisContext: AnalysisContext? = null - private set - - var fileIndex: FileIndex? = null - private set - - var diagnostics: List = emptyList() - private set - - var lastModified: Long = System.currentTimeMillis() - private set - - val isParsed: Boolean get() = parseResult != null - val isAnalyzed: Boolean get() = symbolTable != null - val hasErrors: Boolean get() = diagnostics.any { it.severity == DiagnosticSeverity.ERROR } - - val filePath: String get() = uriToPath(uri) - - val packageName: String get() = symbolTable?.packageName ?: "" - - val lineCount: Int get() = content.count { it == '\n' } + 1 - - /** - * Updates document content and invalidates analysis. - */ - fun updateContent(newContent: String, newVersion: Int) { - content = newContent - version = newVersion - lastModified = System.currentTimeMillis() - invalidate() - } - - /** - * Applies an incremental text change. - */ - fun applyChange( - startLine: Int, - startChar: Int, - endLine: Int, - endChar: Int, - newText: String, - newVersion: Int - ) { - val startOffset = positionToOffset(startLine, startChar) - val endOffset = positionToOffset(endLine, endChar) - - val oldLength = content.length - val deletedText = if (startOffset < endOffset && endOffset <= content.length) { - content.substring(startOffset, endOffset) - } else { - "" - } - - Log.d(TAG, "[DOC-SYNC] applyChange: uri=$uri, range=$startLine:$startChar-$endLine:$endChar") - Log.d(TAG, "[DOC-SYNC] offsets: start=$startOffset, end=$endOffset, contentLength=$oldLength") - Log.d(TAG, "[DOC-SYNC] deleted='$deletedText', newText='$newText' (${newText.length} chars)") - - content = content.substring(0, startOffset) + newText + content.substring(endOffset) - - Log.d(TAG, "[DOC-SYNC] result: oldLen=$oldLength, newLen=${content.length}, version=$newVersion") - Log.d(TAG, "[DOC-SYNC] content near edit: '${content.substring(maxOf(0, startOffset - 10), minOf(content.length, startOffset + 20)).replace("\n", "\\n")}'") - - version = newVersion - lastModified = System.currentTimeMillis() - invalidate() - } - - /** - * Applies an incremental text change using direct character indices. - * This bypasses line/column to offset conversion for more reliable sync. - */ - fun applyChangeByIndex( - startIndex: Int, - endIndex: Int, - newText: String, - newVersion: Int - ) { - val safeStart = maxOf(0, minOf(startIndex, content.length)) - val safeEnd = maxOf(safeStart, minOf(endIndex, content.length)) - - val oldLength = content.length - val deletedText = if (safeStart < safeEnd) { - content.substring(safeStart, safeEnd) - } else { - "" - } - - Log.d(TAG, "[DOC-SYNC] applyChangeByIndex: uri=$uri, indices=$startIndex-$endIndex") - Log.d(TAG, "[DOC-SYNC] safeIndices: start=$safeStart, end=$safeEnd, contentLength=$oldLength") - Log.d(TAG, "[DOC-SYNC] deleted='$deletedText', newText='$newText' (${newText.length} chars)") - - content = content.substring(0, safeStart) + newText + content.substring(safeEnd) - - Log.d(TAG, "[DOC-SYNC] result: oldLen=$oldLength, newLen=${content.length}, version=$newVersion") - Log.d(TAG, "[DOC-SYNC] content near edit: '${content.substring(maxOf(0, safeStart - 10), minOf(content.length, safeStart + 20)).replace("\n", "\\n")}'") - - version = newVersion - lastModified = System.currentTimeMillis() - invalidate() - } - - /** - * Sets parse result after parsing. - */ - fun setParsed(result: ParseResult) { - parseResult = result - syntaxTree = result.tree - } - - /** - * Sets analysis results after semantic analysis. - */ - fun setAnalyzed( - table: SymbolTable, - context: AnalysisContext, - index: FileIndex, - diags: List - ) { - symbolTable = table - analysisContext = context - fileIndex = index - diagnostics = diags - } - - /** - * Invalidates analysis state, preserving diagnostics until new analysis completes. - * - * Diagnostics are intentionally NOT cleared here - they remain visible - * until [setAnalyzed] is called with fresh analysis results. This prevents - * the "diagnostic flicker" where errors disappear during typing and only - * reappear after debounce + analysis time. - */ - fun invalidate() { - parseResult = null - syntaxTree = null - symbolTable = null - analysisContext = null - fileIndex = null - } - - /** - * Explicitly clears diagnostics. Use only when closing documents. - */ - fun clearDiagnostics() { - diagnostics = emptyList() - } - - /** - * Converts line/character position to string offset. - * The character parameter is in UTF-16 code units (LSP specification). - * For Java/Kotlin strings, UTF-16 code units equal the string index for BMP chars. - */ - fun positionToOffset(line: Int, character: Int): Int { - if (content.isEmpty()) return 0 - - var currentLine = 0 - var lineStart = 0 - - for (i in content.indices) { - if (currentLine == line) { - break - } - if (content[i] == '\n') { - currentLine++ - lineStart = i + 1 - } - } - - if (currentLine != line) { - return content.length - } - - return minOf(lineStart + character, content.length) - } - - /** - * Converts string offset to line/character position. - * Returns UTF-16 code unit offset for the column (LSP specification). - * For Java/Kotlin strings, the string index equals UTF-16 code units for BMP chars. - */ - fun offsetToPosition(offset: Int): Pair { - val (line, charColumn) = PositionConverter.charOffsetToLineAndColumn(content, offset) - return line to charColumn - } - - /** - * Gets the line text at a given line number. - */ - fun getLine(lineNumber: Int): String { - val lines = content.split('\n') - return if (lineNumber in lines.indices) lines[lineNumber] else "" - } - - /** - * Gets text in a range. - */ - fun getText(startLine: Int, startChar: Int, endLine: Int, endChar: Int): String { - val start = positionToOffset(startLine, startChar) - val end = positionToOffset(endLine, endChar) - return content.substring(start, end) - } - - /** - * Gets the word at a position. - */ - fun getWordAt(line: Int, character: Int): String? { - val offset = positionToOffset(line, character) - if (offset >= content.length) return null - - var start = offset - var end = offset - - while (start > 0 && isIdentifierChar(content[start - 1])) { - start-- - } - - while (end < content.length && isIdentifierChar(content[end])) { - end++ - } - - return if (start < end) content.substring(start, end) else null - } - - private fun isIdentifierChar(c: Char): Boolean { - return c.isLetterOrDigit() || c == '_' - } - - override fun toString(): String { - return "DocumentState(uri=$uri, version=$version, parsed=$isParsed, analyzed=$isAnalyzed)" - } - - companion object { - fun uriToPath(uri: String): String { - return uri - .removePrefix("file://") - .removePrefix("file:") - .replace("%20", " ") - } - - fun pathToUri(path: String): String { - return "file://$path" - } - } -} - -/** - * Analysis phase for tracking progress. - */ -enum class AnalysisPhase { - NONE, - PARSING, - PARSED, - ANALYZING, - ANALYZED -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinLanguageServer.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinLanguageServer.kt deleted file mode 100644 index e2e1f0707d..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinLanguageServer.kt +++ /dev/null @@ -1,282 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server - -import org.appdevforall.codeonthego.lsp.kotlin.index.ClasspathIndex -import org.appdevforall.codeonthego.lsp.kotlin.index.ClasspathIndexer -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.index.StdlibIndex -import org.appdevforall.codeonthego.lsp.kotlin.index.StdlibIndexLoader -import org.appdevforall.codeonthego.lsp.kotlin.server.providers.SemanticTokenProvider -import org.eclipse.lsp4j.CompletionOptions -import org.eclipse.lsp4j.Diagnostic -import org.eclipse.lsp4j.DiagnosticSeverity -import org.eclipse.lsp4j.InitializeParams -import org.eclipse.lsp4j.InitializeResult -import org.eclipse.lsp4j.InitializedParams -import org.eclipse.lsp4j.MessageParams -import org.eclipse.lsp4j.MessageType -import org.eclipse.lsp4j.Position -import org.eclipse.lsp4j.PublishDiagnosticsParams -import org.eclipse.lsp4j.Range -import org.eclipse.lsp4j.SaveOptions -import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions -import org.eclipse.lsp4j.ServerCapabilities -import org.eclipse.lsp4j.ServerInfo -import org.eclipse.lsp4j.SignatureHelpOptions -import org.eclipse.lsp4j.TextDocumentSyncKind -import org.eclipse.lsp4j.TextDocumentSyncOptions -import org.eclipse.lsp4j.WorkspaceFolder -import org.eclipse.lsp4j.jsonrpc.messages.Either -import org.eclipse.lsp4j.services.LanguageClient -import org.eclipse.lsp4j.services.LanguageClientAware -import org.eclipse.lsp4j.services.LanguageServer -import org.eclipse.lsp4j.services.TextDocumentService -import org.eclipse.lsp4j.services.WorkspaceService -import java.io.File -import java.io.InputStream -import java.util.concurrent.CompletableFuture -import kotlin.system.exitProcess -import org.appdevforall.codeonthego.lsp.kotlin.semantic.Diagnostic as KtDiagnostic -import org.appdevforall.codeonthego.lsp.kotlin.semantic.DiagnosticSeverity as KtDiagnosticSeverity - -private const val TAG = "KotlinLanguageServer" - -/** - * Kotlin Language Server implementation. - * - * KotlinLanguageServer is the main entry point for the LSP protocol, - * implementing the standard Language Server Protocol using LSP4J. - * - * ## Features - * - * - Text document synchronization (incremental) - * - Code completion - * - Hover information - * - Go to definition - * - Find references - * - Document symbols - * - Diagnostics - * - Signature help - * - * ## Usage - * - * ```kotlin - * val server = KotlinLanguageServer() - * - * // For in-process communication - * val launcher = LSPLauncher.createServerLauncher( - * server, - * inputStream, - * outputStream - * ) - * launcher.startListening() - * ``` - */ -class KotlinLanguageServer : LanguageServer, LanguageClientAware { - - private val documentManager = DocumentManager() - private val projectIndex = ProjectIndex() - private val analysisScheduler = AnalysisScheduler(documentManager, projectIndex) - private val classpathIndexer = ClasspathIndexer() - - private lateinit var textDocumentService: KotlinTextDocumentService - private lateinit var workspaceService: KotlinWorkspaceService - - @Volatile - private var client: LanguageClient? = null - - @Volatile - private var initialized = false - - @Volatile - private var shutdownRequested = false - - private var rootUri: String? = null - private var workspaceFolders: List = emptyList() - - init { - textDocumentService = KotlinTextDocumentService( - documentManager = documentManager, - projectIndex = projectIndex, - analysisScheduler = analysisScheduler - ) - - workspaceService = KotlinWorkspaceService( - documentManager = documentManager, - projectIndex = projectIndex, - analysisScheduler = analysisScheduler - ) - } - - fun loadStdlibIndex(inputStream: InputStream) { - val stdlibIndex = StdlibIndexLoader.loadFromStream(inputStream) - projectIndex.setStdlibIndex(stdlibIndex) - } - - fun loadStdlibIndex(stdlibIndex: StdlibIndex) { - projectIndex.setStdlibIndex(stdlibIndex) - } - - fun useMinimalStdlibIndex() { - projectIndex.setStdlibIndex(StdlibIndexLoader.createMinimalIndex()) - } - - fun setClasspath(files: List) { - val index = classpathIndexer.index(files) - projectIndex.setClasspathIndex(index) - } - - fun setClasspathAsync(files: List): CompletableFuture { - return CompletableFuture.supplyAsync { - val index = classpathIndexer.index(files) - projectIndex.setClasspathIndex(index) - index - } - } - - fun addToClasspath(files: List) { - val existingIndex = projectIndex.getClasspathIndex() ?: ClasspathIndex.empty() - val updatedIndex = classpathIndexer.indexIncremental(files, existingIndex) - projectIndex.setClasspathIndex(updatedIndex) - } - - fun getClasspathIndex(): ClasspathIndex? { - return projectIndex.getClasspathIndex() - } - - fun hasAndroidSdkClasses(): Boolean { - val index = projectIndex.getClasspathIndex() ?: return false - return listOf("android.widget.Button", "android.view.View", "android.app.Activity") - .any { index.findByFqName(it) != null } - } - - override fun connect(client: LanguageClient) { - this.client = client - textDocumentService.setClient(client) - workspaceService.setClient(client) - - analysisScheduler.setDiagnosticsListener { update -> - val lsp4jDiags = update.diagnostics.map { it.toLsp4j() } - client.publishDiagnostics( - PublishDiagnosticsParams( - update.uri, - lsp4jDiags, - update.version - ) - ) - } - } - - override fun initialize(params: InitializeParams): CompletableFuture { - rootUri = params.rootUri - workspaceFolders = params.workspaceFolders ?: emptyList() - - val capabilities = ServerCapabilities().apply { - textDocumentSync = Either.forRight(TextDocumentSyncOptions().apply { - openClose = true - change = TextDocumentSyncKind.Incremental - save = Either.forRight(SaveOptions().apply { - includeText = true - }) - }) - - completionProvider = CompletionOptions().apply { - triggerCharacters = listOf(".", ":", "@", "(") - resolveProvider = true - } - - hoverProvider = Either.forLeft(true) - - definitionProvider = Either.forLeft(true) - - referencesProvider = Either.forLeft(true) - - documentSymbolProvider = Either.forLeft(true) - - workspaceSymbolProvider = Either.forLeft(true) - - signatureHelpProvider = SignatureHelpOptions().apply { - triggerCharacters = listOf("(", ",") - retriggerCharacters = listOf(",") - } - - codeActionProvider = Either.forLeft(true) - - semanticTokensProvider = SemanticTokensWithRegistrationOptions().apply { - legend = SemanticTokenProvider.LEGEND - full = Either.forLeft(true) - } - } - - val serverInfo = ServerInfo("Kotlin Language Server", "0.1.0") - - analysisScheduler.start() - initialized = true - - return CompletableFuture.completedFuture(InitializeResult(capabilities, serverInfo)) - } - - override fun initialized(params: InitializedParams?) { - client?.logMessage(MessageParams(MessageType.Info, "Kotlin Language Server initialized")) - } - - override fun shutdown(): CompletableFuture { - shutdownRequested = true - analysisScheduler.stop() - documentManager.clear() - return CompletableFuture.completedFuture(null) - } - - override fun exit() { - if (!shutdownRequested) { - exitProcess(1) - } else { - exitProcess(0) - } - } - - override fun getTextDocumentService(): TextDocumentService { - return textDocumentService - } - - override fun getWorkspaceService(): WorkspaceService { - return workspaceService - } - - fun getDocumentManager(): DocumentManager = documentManager - - fun getProjectIndex(): ProjectIndex = projectIndex - - fun getAnalysisScheduler(): AnalysisScheduler = analysisScheduler - - fun didChangeByIndex(uri: String, startIndex: Int, endIndex: Int, newText: String, version: Int) { - textDocumentService.didChangeByIndex(uri, startIndex, endIndex, newText, version) - } - - fun isInitialized(): Boolean = initialized - - fun isShutdownRequested(): Boolean = shutdownRequested - - fun getClient(): LanguageClient? = client - - fun getRootUri(): String? = rootUri - - fun getWorkspaceFolders(): List = workspaceFolders -} - -fun KtDiagnostic.toLsp4j(): Diagnostic { - return Diagnostic().apply { - range = Range( - Position(this@toLsp4j.range.startLine, this@toLsp4j.range.startColumn), - Position(this@toLsp4j.range.endLine, this@toLsp4j.range.endColumn) - ) - severity = when (this@toLsp4j.severity) { - KtDiagnosticSeverity.ERROR -> DiagnosticSeverity.Error - KtDiagnosticSeverity.WARNING -> DiagnosticSeverity.Warning - KtDiagnosticSeverity.INFO -> DiagnosticSeverity.Information - KtDiagnosticSeverity.HINT -> DiagnosticSeverity.Hint - } - source = "ktlsp" - message = this@toLsp4j.message - code = Either.forLeft(this@toLsp4j.code.name) - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinTextDocumentService.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinTextDocumentService.kt deleted file mode 100644 index 51476e7dc2..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinTextDocumentService.kt +++ /dev/null @@ -1,263 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server - -import android.util.Log -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.server.providers.* -import org.eclipse.lsp4j.* -import org.eclipse.lsp4j.jsonrpc.messages.Either -import org.eclipse.lsp4j.jsonrpc.messages.Either3 -import org.eclipse.lsp4j.services.LanguageClient -import org.eclipse.lsp4j.services.TextDocumentService -import java.util.concurrent.CompletableFuture - -private const val TAG = "KtTextDocService" - -/** - * Handles text document related LSP requests. - * - * KotlinTextDocumentService processes all document lifecycle events - * and provides language features like completion, hover, and navigation. - */ -class KotlinTextDocumentService( - private val documentManager: DocumentManager, - private val projectIndex: ProjectIndex, - private val analysisScheduler: AnalysisScheduler -) : TextDocumentService { - - @Volatile - private var client: LanguageClient? = null - - private val completionProvider = CompletionProvider(documentManager, projectIndex, analysisScheduler) - private val hoverProvider = HoverProvider(documentManager, projectIndex, analysisScheduler) - private val definitionProvider = DefinitionProvider(documentManager, projectIndex, analysisScheduler) - private val documentSymbolProvider = DocumentSymbolProvider(documentManager, analysisScheduler) - private val diagnosticProvider = DiagnosticProvider(documentManager, analysisScheduler) - private val semanticTokenProvider = SemanticTokenProvider(documentManager, projectIndex, analysisScheduler) - private val codeActionProvider = CodeActionProvider(documentManager, projectIndex, analysisScheduler) - - fun setClient(client: LanguageClient) { - this.client = client - diagnosticProvider.setClient(client) - } - - override fun didOpen(params: DidOpenTextDocumentParams) { - val document = params.textDocument - Log.d(TAG, "didOpen: uri=${document.uri}, version=${document.version}, contentLength=${document.text.length}") - - val existing = documentManager.get(document.uri) - if (existing != null) { - Log.w(TAG, " WARNING: document already open! existing version=${existing.version}, contentLength=${existing.content.length}") - } - - documentManager.open(document.uri, document.text, document.version) - analysisScheduler.scheduleImmediateAnalysis(document.uri) - } - - override fun didChange(params: DidChangeTextDocumentParams) { - val uri = params.textDocument.uri - val version = params.textDocument.version - - Log.d(TAG, "didChange: uri=$uri, version=$version, changes=${params.contentChanges.size}") - - for ((index, change) in params.contentChanges.withIndex()) { - if (change.range == null) { - Log.d(TAG, " change[$index]: full sync, text=${change.text.length} chars") - documentManager.update(uri, change.text, version) - } else { - val range = change.range - Log.d(TAG, " change[$index]: incremental, range=${range.start.line}:${range.start.character}-${range.end.line}:${range.end.character}, text='${change.text}' (${change.text.length} chars)") - documentManager.applyChange( - uri = uri, - startLine = range.start.line, - startChar = range.start.character, - endLine = range.end.line, - endChar = range.end.character, - newText = change.text, - version = version - ) - } - } - - val state = documentManager.get(uri) - Log.d(TAG, " after changes: contentLength=${state?.content?.length ?: -1}") - - analysisScheduler.scheduleAnalysis(uri) - } - - /** - * Applies a document change using direct character indices. - * This bypasses line/column to offset conversion for more reliable sync. - */ - fun didChangeByIndex( - uri: String, - startIndex: Int, - endIndex: Int, - newText: String, - version: Int - ) { - Log.d(TAG, "didChangeByIndex: uri=$uri, version=$version, indices=$startIndex-$endIndex, text='$newText' (${newText.length} chars)") - - documentManager.applyChangeByIndex( - uri = uri, - startIndex = startIndex, - endIndex = endIndex, - newText = newText, - version = version - ) - - val state = documentManager.get(uri) - Log.d(TAG, " after change: contentLength=${state?.content?.length ?: -1}") - - analysisScheduler.scheduleAnalysis(uri) - } - - override fun didClose(params: DidCloseTextDocumentParams) { - val uri = params.textDocument.uri - analysisScheduler.cancelAnalysis(uri) - documentManager.close(uri) - projectIndex.removeFile(uri) - - client?.publishDiagnostics(PublishDiagnosticsParams(uri, emptyList())) - } - - override fun didSave(params: DidSaveTextDocumentParams) { - val uri = params.textDocument.uri - - if (params.text != null) { - val state = documentManager.get(uri) - if (state != null) { - documentManager.update(uri, params.text, state.version + 1) - } - } - - analysisScheduler.scheduleImmediateAnalysis(uri) - } - - override fun completion( - params: CompletionParams - ): CompletableFuture, CompletionList>> { - return CompletableFuture.supplyAsync { - val result = completionProvider.provideCompletions( - uri = params.textDocument.uri, - line = params.position.line, - character = params.position.character, - context = params.context - ) - Either.forRight(result) - } - } - - override fun resolveCompletionItem( - unresolved: CompletionItem - ): CompletableFuture { - return CompletableFuture.supplyAsync { - completionProvider.resolveCompletionItem(unresolved) - } - } - - override fun hover(params: HoverParams): CompletableFuture { - return CompletableFuture.supplyAsync { - hoverProvider.provideHover( - uri = params.textDocument.uri, - line = params.position.line, - character = params.position.character - ) - } - } - - override fun definition( - params: DefinitionParams - ): CompletableFuture, List>> { - return CompletableFuture.supplyAsync { - val locations = definitionProvider.provideDefinition( - uri = params.textDocument.uri, - line = params.position.line, - character = params.position.character - ) - Either.forLeft(locations) - } - } - - override fun references(params: ReferenceParams): CompletableFuture> { - return CompletableFuture.supplyAsync { - definitionProvider.provideReferences( - uri = params.textDocument.uri, - line = params.position.line, - character = params.position.character, - includeDeclaration = params.context?.isIncludeDeclaration == true - ) - } - } - - override fun documentSymbol( - params: DocumentSymbolParams - ): CompletableFuture>> { - return CompletableFuture.supplyAsync { - val symbols = documentSymbolProvider.provideDocumentSymbols(params.textDocument.uri) - symbols.map { Either.forRight(it) } - } - } - - override fun signatureHelp(params: SignatureHelpParams): CompletableFuture { - return CompletableFuture.supplyAsync { - completionProvider.provideSignatureHelp( - uri = params.textDocument.uri, - line = params.position.line, - character = params.position.character - ) - } - } - - override fun codeAction(params: CodeActionParams): CompletableFuture>> { - return CompletableFuture.supplyAsync { - codeActionProvider.provideCodeActions( - uri = params.textDocument.uri, - range = params.range, - diagnostics = params.context?.diagnostics ?: emptyList() - ) - } - } - - override fun documentHighlight( - params: DocumentHighlightParams - ): CompletableFuture> { - return CompletableFuture.supplyAsync { - definitionProvider.provideHighlights( - uri = params.textDocument.uri, - line = params.position.line, - character = params.position.character - ) - } - } - - override fun semanticTokensFull( - params: SemanticTokensParams - ): CompletableFuture { - return CompletableFuture.supplyAsync { - semanticTokenProvider.provideSemanticTokens(params.textDocument.uri) - ?: SemanticTokens(emptyList()) - } - } - - override fun rename(params: RenameParams): CompletableFuture { - return CompletableFuture.completedFuture(null) - } - - override fun prepareRename( - params: PrepareRenameParams - ): CompletableFuture?> { - return CompletableFuture.completedFuture(null) - } - - override fun formatting(params: DocumentFormattingParams): CompletableFuture> { - return CompletableFuture.completedFuture(emptyList()) - } - - override fun rangeFormatting(params: DocumentRangeFormattingParams): CompletableFuture> { - return CompletableFuture.completedFuture(emptyList()) - } - - override fun onTypeFormatting(params: DocumentOnTypeFormattingParams): CompletableFuture> { - return CompletableFuture.completedFuture(emptyList()) - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinWorkspaceService.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinWorkspaceService.kt deleted file mode 100644 index fe8ace2ef2..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/KotlinWorkspaceService.kt +++ /dev/null @@ -1,162 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server - -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbolKind -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.eclipse.lsp4j.* -import org.eclipse.lsp4j.jsonrpc.messages.Either -import org.eclipse.lsp4j.services.LanguageClient -import org.eclipse.lsp4j.services.WorkspaceService -import java.util.concurrent.CompletableFuture - -/** - * Handles workspace-level LSP requests. - * - * KotlinWorkspaceService processes workspace operations like - * symbol search and configuration changes. - */ -class KotlinWorkspaceService( - private val documentManager: DocumentManager, - private val projectIndex: ProjectIndex, - private val analysisScheduler: AnalysisScheduler -) : WorkspaceService { - - @Volatile - private var client: LanguageClient? = null - - private var workspaceFolders: List = emptyList() - - fun setClient(client: LanguageClient) { - this.client = client - } - - fun setWorkspaceFolders(folders: List) { - workspaceFolders = folders - } - - override fun symbol( - params: WorkspaceSymbolParams - ): CompletableFuture, List>> { - return CompletableFuture.supplyAsync { - val query = params.query - if (query.isBlank()) { - return@supplyAsync Either.forLeft(emptyList()) - } - - val symbols = projectIndex.findByPrefix(query, limit = 50) - - val symbolInfos = symbols.mapNotNull { indexed -> - val filePath = indexed.filePath ?: return@mapNotNull null - - val startLine = indexed.startLine ?: 0 - val startColumn = indexed.startColumn ?: 0 - val endLine = indexed.endLine ?: startLine - val endColumn = indexed.endColumn ?: (startColumn + indexed.name.length) - - SymbolInformation().apply { - name = indexed.name - kind = indexed.kind.toSymbolKind() - location = Location( - filePath, - Range(Position(startLine, startColumn), Position(endLine, endColumn)) - ) - containerName = indexed.containingClass ?: indexed.packageName - if (indexed.deprecated) { - @Suppress("DEPRECATION") - deprecated = true - } - } - } - - Either.forLeft(symbolInfos) - } - } - - override fun didChangeConfiguration(params: DidChangeConfigurationParams) { - client?.logMessage(MessageParams(MessageType.Info, "Configuration changed")) - } - - override fun didChangeWatchedFiles(params: DidChangeWatchedFilesParams) { - for (change in params.changes) { - when (change.type) { - FileChangeType.Created -> handleFileCreated(change.uri) - FileChangeType.Changed -> handleFileChanged(change.uri) - FileChangeType.Deleted -> handleFileDeleted(change.uri) - else -> {} - } - } - } - - override fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) { - val added = params.event.added ?: emptyList() - val removed = params.event.removed ?: emptyList() - - val updatedFolders = workspaceFolders.toMutableList() - updatedFolders.removeAll(removed) - updatedFolders.addAll(added) - workspaceFolders = updatedFolders - - for (folder in removed) { - handleWorkspaceFolderRemoved(folder) - } - - for (folder in added) { - handleWorkspaceFolderAdded(folder) - } - } - - override fun executeCommand(params: ExecuteCommandParams): CompletableFuture { - return CompletableFuture.completedFuture(null) - } - - private fun handleFileCreated(uri: String) { - if (!uri.endsWith(".kt") && !uri.endsWith(".kts")) return - analysisScheduler.scheduleAnalysis(uri) - } - - private fun handleFileChanged(uri: String) { - if (!uri.endsWith(".kt") && !uri.endsWith(".kts")) return - - if (!documentManager.isOpen(uri)) { - analysisScheduler.scheduleAnalysis(uri) - } - } - - private fun handleFileDeleted(uri: String) { - documentManager.close(uri) - projectIndex.removeFile(uri) - client?.publishDiagnostics(PublishDiagnosticsParams(uri, emptyList())) - } - - private fun handleWorkspaceFolderAdded(folder: WorkspaceFolder) { - client?.logMessage(MessageParams(MessageType.Info, "Workspace folder added: ${folder.uri}")) - } - - private fun handleWorkspaceFolderRemoved(folder: WorkspaceFolder) { - val folderUri = folder.uri - - for (uri in documentManager.openUris) { - if (uri.startsWith(folderUri)) { - documentManager.close(uri) - projectIndex.removeFile(uri) - } - } - - client?.logMessage(MessageParams(MessageType.Info, "Workspace folder removed: ${folder.uri}")) - } -} - -private fun IndexedSymbolKind.toSymbolKind(): SymbolKind { - return when (this) { - IndexedSymbolKind.CLASS -> SymbolKind.Class - IndexedSymbolKind.INTERFACE -> SymbolKind.Interface - IndexedSymbolKind.OBJECT -> SymbolKind.Object - IndexedSymbolKind.ENUM_CLASS -> SymbolKind.Enum - IndexedSymbolKind.ANNOTATION_CLASS -> SymbolKind.Class - IndexedSymbolKind.DATA_CLASS -> SymbolKind.Class - IndexedSymbolKind.VALUE_CLASS -> SymbolKind.Class - IndexedSymbolKind.FUNCTION -> SymbolKind.Function - IndexedSymbolKind.PROPERTY -> SymbolKind.Property - IndexedSymbolKind.CONSTRUCTOR -> SymbolKind.Constructor - IndexedSymbolKind.TYPE_ALIAS -> SymbolKind.TypeParameter - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CodeActionProvider.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CodeActionProvider.kt deleted file mode 100644 index a9787c6023..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CodeActionProvider.kt +++ /dev/null @@ -1,214 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server.providers - -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.semantic.DiagnosticCode -import org.appdevforall.codeonthego.lsp.kotlin.server.AnalysisScheduler -import org.appdevforall.codeonthego.lsp.kotlin.server.DocumentManager -import org.eclipse.lsp4j.* -import org.eclipse.lsp4j.jsonrpc.messages.Either - -class CodeActionProvider( - private val documentManager: DocumentManager, - private val projectIndex: ProjectIndex, - private val analysisScheduler: AnalysisScheduler -) { - fun provideCodeActions( - uri: String, - range: Range, - diagnostics: List - ): List> { - val state = documentManager.get(uri) ?: return emptyList() - - analysisScheduler.analyzeSync(uri) - - val actions = mutableListOf>() - - for (diagnostic in diagnostics) { - val diagnosticActions = createActionsForDiagnostic(uri, diagnostic, state.content) - actions.addAll(diagnosticActions.map { Either.forRight(it) }) - } - - return actions - } - - private fun createActionsForDiagnostic( - uri: String, - diagnostic: Diagnostic, - content: String - ): List { - val code = diagnostic.code?.left ?: return emptyList() - - return when (code) { - DiagnosticCode.UNRESOLVED_REFERENCE.id -> createImportActions(uri, diagnostic) - DiagnosticCode.UNSAFE_CALL.id -> createSafeCallActions(uri, diagnostic, content) - DiagnosticCode.VAL_REASSIGNMENT.id -> createChangeToVarAction(uri, diagnostic, content) - else -> emptyList() - } - } - - private fun createImportActions(uri: String, diagnostic: Diagnostic): List { - val unresolvedName = extractUnresolvedName(diagnostic.message) ?: return emptyList() - - val candidates = projectIndex.findBySimpleName(unresolvedName) - .filter { it.fqName != unresolvedName } - .distinctBy { it.fqName } - .take(5) - - if (candidates.isEmpty()) { - val stdlibCandidates = projectIndex.getStdlibIndex() - ?.findBySimpleName(unresolvedName) - ?.distinctBy { it.fqName } - ?.take(5) - ?: emptyList() - - return stdlibCandidates.map { candidate -> - createImportAction(uri, candidate.fqName) - } - } - - return candidates.map { candidate -> - createImportAction(uri, candidate.fqName) - } - } - - private fun createImportAction(uri: String, fqName: String): CodeAction { - val state = documentManager.get(uri) ?: return CodeAction().apply { - title = "Import '$fqName'" - kind = CodeActionKind.QuickFix - } - - val importPosition = findImportInsertPosition(state.content) - - val edit = TextEdit( - Range(Position(importPosition, 0), Position(importPosition, 0)), - "import $fqName\n" - ) - - return CodeAction().apply { - title = "Import '$fqName'" - kind = CodeActionKind.QuickFix - this.edit = WorkspaceEdit(mapOf(uri to listOf(edit))) - } - } - - private fun findImportInsertPosition(content: String): Int { - val lines = content.lines() - - var lastImportLine = -1 - var packageLine = -1 - - for ((index, line) in lines.withIndex()) { - val trimmed = line.trim() - when { - trimmed.startsWith("package ") -> packageLine = index - trimmed.startsWith("import ") -> lastImportLine = index - } - } - - return when { - lastImportLine >= 0 -> lastImportLine + 1 - packageLine >= 0 -> packageLine + 2 - else -> 0 - } - } - - private fun createSafeCallActions( - uri: String, - diagnostic: Diagnostic, - content: String - ): List { - val range = diagnostic.range - val lines = content.lines() - - if (range.start.line >= lines.size) return emptyList() - - val line = lines[range.start.line] - val dotIndex = findDotBeforePosition(line, range.start.character) - if (dotIndex < 0) return emptyList() - - val safeCallEdit = TextEdit( - Range(Position(range.start.line, dotIndex), Position(range.start.line, dotIndex + 1)), - "?." - ) - - val safeCallAction = CodeAction().apply { - title = "Use safe call (?.)" - kind = CodeActionKind.QuickFix - edit = WorkspaceEdit(mapOf(uri to listOf(safeCallEdit))) - diagnostics = listOf(diagnostic) - } - - val assertionEdit = TextEdit( - Range(Position(range.start.line, dotIndex), Position(range.start.line, dotIndex + 1)), - "!!." - ) - - val assertionAction = CodeAction().apply { - title = "Use non-null assertion (!!.)" - kind = CodeActionKind.QuickFix - edit = WorkspaceEdit(mapOf(uri to listOf(assertionEdit))) - diagnostics = listOf(diagnostic) - } - - return listOf(safeCallAction, assertionAction) - } - - private fun findDotBeforePosition(line: String, position: Int): Int { - for (i in position downTo 0) { - if (i < line.length && line[i] == '.') { - return i - } - } - return -1 - } - - private fun createChangeToVarAction( - uri: String, - diagnostic: Diagnostic, - content: String - ): List { - val state = documentManager.get(uri) ?: return emptyList() - val symbolTable = state.symbolTable ?: return emptyList() - - val position = diagnostic.range.start - val lines = content.lines() - if (position.line >= lines.size) return emptyList() - - val valPosition = findValDeclaration(content, position.line, position.character) - if (valPosition == null) return emptyList() - - val edit = TextEdit( - Range( - Position(valPosition.first, valPosition.second), - Position(valPosition.first, valPosition.second + 3) - ), - "var" - ) - - return listOf(CodeAction().apply { - title = "Change 'val' to 'var'" - kind = CodeActionKind.QuickFix - this.edit = WorkspaceEdit(mapOf(uri to listOf(edit))) - diagnostics = listOf(diagnostic) - }) - } - - private fun findValDeclaration(content: String, startLine: Int, startChar: Int): Pair? { - val lines = content.lines() - - for (line in startLine downTo maxOf(0, startLine - 20)) { - val lineContent = lines.getOrNull(line) ?: continue - val valIndex = lineContent.indexOf("val ") - if (valIndex >= 0) { - return line to valIndex - } - } - - return null - } - - private fun extractUnresolvedName(message: String): String? { - val pattern = "Unresolved reference: (.+)".toRegex() - return pattern.find(message)?.groupValues?.getOrNull(1) - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CompletionProvider.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CompletionProvider.kt deleted file mode 100644 index 031ec3a0b6..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/CompletionProvider.kt +++ /dev/null @@ -1,1267 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server.providers - -import org.appdevforall.codeonthego.lsp.kotlin.index.ClasspathIndex -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbol -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbolKind -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.index.StdlibIndex -import org.appdevforall.codeonthego.lsp.kotlin.parser.KotlinParser -import org.appdevforall.codeonthego.lsp.kotlin.parser.Position -import org.appdevforall.codeonthego.lsp.kotlin.semantic.AnalysisContext -import org.appdevforall.codeonthego.lsp.kotlin.server.AnalysisScheduler -import org.appdevforall.codeonthego.lsp.kotlin.server.DocumentManager -import org.appdevforall.codeonthego.lsp.kotlin.server.DocumentState -import org.appdevforall.codeonthego.lsp.kotlin.symbol.* -import org.eclipse.lsp4j.* -import org.eclipse.lsp4j.jsonrpc.messages.Either - -/** - * Provides code completion suggestions. - */ -class CompletionProvider( - private val documentManager: DocumentManager, - private val projectIndex: ProjectIndex, - private val analysisScheduler: AnalysisScheduler -) { - fun provideCompletions( - uri: String, - line: Int, - character: Int, - context: CompletionContext? - ): CompletionList { - val state = documentManager.get(uri) ?: return CompletionList(false, emptyList()) - - val content = state.content - val triggerChar = context?.triggerCharacter - - val patchResult = analyzeWithCompletionPatch(state, uri, line) - val symbolTable = patchResult.first - val analysisContext = patchResult.second - - val completionContext = analyzeCompletionContext(content, line, character, triggerChar) - - val items = when (completionContext) { - is CompletionKind.MemberAccess -> provideMemberCompletions( - completionContext.receiver, - completionContext.prefix, - symbolTable, - analysisContext, - line, - character - ) - is CompletionKind.TypeAnnotation -> provideTypeCompletions( - completionContext.prefix, - symbolTable - ) - is CompletionKind.Import -> provideImportCompletions( - completionContext.prefix - ) - is CompletionKind.Statement -> provideStatementCompletions( - completionContext.prefix, - symbolTable, - analysisContext, - line, - character - ) - is CompletionKind.Annotation -> provideAnnotationCompletions( - completionContext.prefix - ) - } - - return CompletionList(false, items) - } - - fun resolveCompletionItem(item: CompletionItem): CompletionItem { - val data = item.data - if (data is String) { - val symbol = projectIndex.findByFqName(data) - if (symbol != null) { - item.documentation = Either.forRight(MarkupContent().apply { - kind = MarkupKind.MARKDOWN - value = buildDocumentation(symbol) - }) - } - } - return item - } - - fun provideSignatureHelp(uri: String, line: Int, character: Int): SignatureHelp? { - val state = documentManager.get(uri) ?: return null - - analysisScheduler.analyzeSync(uri) - - val content = state.content - val offset = state.positionToOffset(line, character) - - val callInfo = findCallContext(content, offset) ?: return null - - val functions = projectIndex.findByPrefix(callInfo.functionName, limit = 20) - .filter { it.kind == IndexedSymbolKind.FUNCTION && it.name == callInfo.functionName } - - if (functions.isEmpty()) return null - - val signatures = functions.map { func -> - SignatureInformation().apply { - label = buildSignatureLabel(func) - documentation = Either.forRight(MarkupContent().apply { - kind = MarkupKind.MARKDOWN - value = "```kotlin\n${func.toDisplayString()}\n```" - }) - parameters = func.parameters.map { param -> - ParameterInformation().apply { - label = Either.forLeft("${param.name}: ${param.type}") - } - } - } - } - - return SignatureHelp().apply { - this.signatures = signatures - activeSignature = 0 - activeParameter = callInfo.argumentIndex - } - } - - private fun analyzeCompletionContext( - content: String, - line: Int, - character: Int, - triggerChar: String? - ): CompletionKind { - val lines = content.split('\n') - if (line >= lines.size) return CompletionKind.Statement("") - - val lineText = lines[line] - val beforeCursor = if (character <= lineText.length) lineText.substring(0, character) else lineText - - val trimmedLine = beforeCursor.trimStart() - if (trimmedLine.startsWith("import ") || trimmedLine == "import") { - val importPart = if (trimmedLine == "import") "" else trimmedLine.substringAfter("import ").trim() - return CompletionKind.Import(importPart) - } - - if (triggerChar == ".") { - val receiver = extractReceiverExpression(beforeCursor.dropLast(1)) - return CompletionKind.MemberAccess(receiver, "") - } - - if (triggerChar == ":") { - if (beforeCursor.trimEnd().endsWith(":")) { - return CompletionKind.TypeAnnotation("") - } - } - - if (triggerChar == "@") { - return CompletionKind.Annotation("") - } - - val dotIndex = beforeCursor.lastIndexOf('.') - if (dotIndex >= 0) { - val afterDot = beforeCursor.substring(dotIndex + 1) - if (afterDot.all { it.isLetterOrDigit() || it == '_' }) { - val receiver = extractReceiverExpression(beforeCursor.substring(0, dotIndex)) - return CompletionKind.MemberAccess(receiver, afterDot) - } - } - - val colonIndex = beforeCursor.lastIndexOf(':') - if (colonIndex >= 0) { - val afterColon = beforeCursor.substring(colonIndex + 1).trim() - if (afterColon.all { it.isLetterOrDigit() || it == '_' || it == '.' }) { - return CompletionKind.TypeAnnotation(afterColon) - } - } - - val prefix = extractIdentifierPrefix(beforeCursor) - return CompletionKind.Statement(prefix) - } - - private fun extractReceiverExpression(text: String): String { - val trimmed = text.trimEnd() - if (trimmed.isEmpty()) return "" - - var depth = 0 - var i = trimmed.length - 1 - - while (i >= 0) { - when (trimmed[i]) { - ')' -> depth++ - '(' -> { - depth-- - if (depth < 0) break - } - ']' -> depth++ - '[' -> { - depth-- - if (depth < 0) break - } - ' ', '\t', '\n', '=', ',', ';', '{', '}' -> { - if (depth == 0) break - } - } - i-- - } - - return trimmed.substring(i + 1) - } - - private fun extractIdentifierPrefix(text: String): String { - var i = text.length - 1 - while (i >= 0 && (text[i].isLetterOrDigit() || text[i] == '_')) { - i-- - } - return text.substring(i + 1) - } - - private fun provideMemberCompletions( - receiver: String, - prefix: String, - symbolTable: SymbolTable?, - analysisContext: AnalysisContext?, - line: Int, - character: Int - ): List { - val items = mutableListOf() - val addedMemberNames = mutableSetOf() - - val typeRef = resolveReceiverType(receiver, symbolTable, analysisContext, line, character) - val cleanReceiver = receiver.trimEnd('?') - val typeName = typeRef?.name ?: cleanReceiver - - if (symbolTable != null) { - val position = Position(line, character) - val symbols = symbolTable.resolve(cleanReceiver, position) - val symbol = symbols.firstOrNull() - - when (symbol) { - is ClassSymbol -> { - when { - symbol.kind == ClassKind.OBJECT || symbol.kind == ClassKind.COMPANION_OBJECT -> { - symbol.members.forEach { member -> - addedMemberNames.add(member.name) - items.add(createCompletionItem(member, CompletionItemPriority.MEMBER, analysisContext)) - } - } - symbol.kind == ClassKind.ENUM_CLASS -> { - symbol.members.forEach { member -> - addedMemberNames.add(member.name) - items.add(createCompletionItem(member, CompletionItemPriority.MEMBER, analysisContext)) - } - } - else -> { - val companion = symbol.companionObject - ?: symbol.members.filterIsInstance() - .find { it.kind == ClassKind.COMPANION_OBJECT } - companion?.members?.forEach { member -> - addedMemberNames.add(member.name) - items.add(createCompletionItem(member, CompletionItemPriority.MEMBER, analysisContext)) - } - } - } - } - is PropertySymbol, is ParameterSymbol -> { - val resolvedType = typeRef?.name - if (resolvedType != null) { - val classSymbol = symbolTable.resolve(resolvedType, position).firstOrNull() - if (classSymbol is ClassSymbol) { - classSymbol.members.forEach { member -> - addedMemberNames.add(member.name) - items.add(createCompletionItem(member, CompletionItemPriority.MEMBER, analysisContext)) - } - - addInheritedMembers( - classSymbol, symbolTable, analysisContext, position, - items, addedMemberNames - ) - } - } - } - else -> {} - } - } - - val classpathIndex = projectIndex.getClasspathIndex() - if (classpathIndex != null) { - val fqName = resolveToFqName(typeName, symbolTable) - val classpathMembers = findClasspathMembersWithInheritance(fqName, classpathIndex) - classpathMembers.forEach { member -> - if (member.name !in addedMemberNames && !isFilteredMember(member.name)) { - addedMemberNames.add(member.name) - items.add(createCompletionItemFromIndexed(member, CompletionItemPriority.MEMBER)) - } - } - - for (member in classpathMembers) { - val name = member.name - val propName = when { - name.startsWith("get") && name.length > 3 && name[3].isUpperCase() -> - name[3].lowercase() + name.substring(4) - name.startsWith("is") && name.length > 2 && name[2].isUpperCase() -> - name[2].lowercase() + name.substring(3) - else -> null - } - if (propName != null && propName !in addedMemberNames) { - addedMemberNames.add(propName) - items.add(CompletionItem().apply { - label = propName - kind = CompletionItemKind.Property - detail = member.returnType ?: "" - sortText = "${CompletionItemPriority.MEMBER.ordinal}_$propName" - insertText = propName - }) - } - } - } - - val projectExtensions = projectIndex.findExtensions(typeName, emptyList()) - projectExtensions.forEach { ext -> - if (!ext.isStdlib) { - items.add(createCompletionItemFromIndexed(ext, CompletionItemPriority.EXTENSION)) - } - } - - val stdlibIndex = projectIndex.getStdlibIndex() - - if (stdlibIndex != null) { - val simpleTypeName = typeName.substringAfterLast('.') - - val stdlibMembers = findStdlibMembersWithInheritance(simpleTypeName, stdlibIndex) - stdlibMembers.forEach { member -> - if (member.name !in addedMemberNames) { - addedMemberNames.add(member.name) - items.add(createCompletionItemFromIndexed(member, CompletionItemPriority.MEMBER)) - } - } - - val typeExtensions = stdlibIndex.findExtensions(simpleTypeName, emptyList()) - typeExtensions.forEach { ext -> - if (ext.name !in addedMemberNames) { - addedMemberNames.add(ext.name) - items.add(createCompletionItemFromIndexed(ext, CompletionItemPriority.MEMBER)) - } - } - - val typeClass = stdlibIndex.findBySimpleName(simpleTypeName) - .firstOrNull { it.kind.isClass } - - if (typeClass != null) { - typeClass.superTypes.forEach { superType -> - val superSimpleName = superType.substringAfterLast('.') - val superExtensions = stdlibIndex.findExtensions(superSimpleName, emptyList()) - superExtensions.forEach { ext -> - if (ext.name !in addedMemberNames) { - addedMemberNames.add(ext.name) - items.add(createCompletionItemFromIndexed(ext, CompletionItemPriority.MEMBER)) - } - } - } - } - } - - val stdlibExtensions = stdlibIndex?.findExtensions(typeName, emptyList()) ?: emptyList() - stdlibExtensions.forEach { ext -> - if (items.none { it.label == ext.name }) { - items.add(createCompletionItemFromIndexed(ext, CompletionItemPriority.STDLIB)) - } - } - - return if (prefix.isNotEmpty()) { - items.filter { it.label.startsWith(prefix, ignoreCase = true) } - } else { - items - } - } - - private fun provideTypeCompletions(prefix: String, symbolTable: SymbolTable?): List { - val items = mutableListOf() - val addedNames = mutableSetOf() - - symbolTable?.topLevelSymbols - ?.filterIsInstance() - ?.filter { prefix.isEmpty() || it.name.startsWith(prefix, ignoreCase = true) } - ?.forEach { cls -> - addedNames.add(cls.name) - items.add(createCompletionItem(cls, CompletionItemPriority.LOCAL)) - } - - val classpathIndex = projectIndex.getClasspathIndex() - val classpathTypes = classpathIndex?.getAllClasses() - ?.filter { prefix.isEmpty() || it.name.startsWith(prefix, ignoreCase = true) } - ?.sortedWith(compareBy { if (it.name.equals(prefix, ignoreCase = true)) 0 else 1 }.thenBy { it.name }) - ?: emptyList() - - val stdlibTypes = projectIndex.getStdlibIndex()?.getAllClasses() - ?.filter { prefix.isEmpty() || it.name.startsWith(prefix, ignoreCase = true) } - ?.sortedWith(compareBy { if (it.name.equals(prefix, ignoreCase = true)) 0 else 1 }.thenBy { it.name }) - ?: emptyList() - - classpathTypes.take(50).forEach { type -> - if (type.name !in addedNames) { - addedNames.add(type.name) - items.add(createCompletionItemFromIndexed(type, CompletionItemPriority.IMPORTED)) - } - } - - stdlibTypes.take(30).forEach { type -> - if (type.name !in addedNames) { - addedNames.add(type.name) - items.add(createCompletionItemFromIndexed(type, CompletionItemPriority.STDLIB)) - } - } - - return items - } - - private fun provideImportCompletions(prefix: String): List { - val items = mutableListOf() - val addedNames = mutableSetOf() - - val packageName: String - val incomplete: String - - when { - prefix.isEmpty() -> { - packageName = "" - incomplete = "" - } - prefix.endsWith(".") -> { - packageName = prefix.dropLast(1) - incomplete = "" - } - prefix.contains(".") -> { - packageName = prefix.substringBeforeLast('.') - incomplete = prefix.substringAfterLast('.') - } - else -> { - packageName = "" - incomplete = prefix - } - } - - val allPackages = projectIndex.packageNames - - if (packageName.isEmpty()) { - val rootPackages = allPackages - .map { it.substringBefore('.') } - .filter { it.isNotEmpty() } - .distinct() - .filter { incomplete.isEmpty() || it.startsWith(incomplete, ignoreCase = true) } - .sorted() - - rootPackages.take(50).forEach { pkg -> - if (pkg !in addedNames) { - addedNames.add(pkg) - items.add(CompletionItem().apply { - label = pkg - kind = CompletionItemKind.Module - insertText = pkg - sortText = "0_$pkg" - }) - } - } - } else { - val subpackagePrefix = "$packageName." - val subpackages = allPackages - .filter { it.startsWith(subpackagePrefix) && it != packageName } - .map { pkg -> - val rest = pkg.removePrefix(subpackagePrefix) - val firstDot = rest.indexOf('.') - if (firstDot > 0) rest.substring(0, firstDot) else rest - } - .distinct() - .filter { incomplete.isEmpty() || it.startsWith(incomplete, ignoreCase = true) } - .sorted() - - subpackages.take(30).forEach { subPkg -> - if (subPkg !in addedNames) { - addedNames.add(subPkg) - items.add(CompletionItem().apply { - label = subPkg - kind = CompletionItemKind.Module - insertText = subPkg - sortText = "0_$subPkg" - }) - } - } - - val classesInPackage = projectIndex.findByPackage(packageName) - .filter { it.isTopLevel && it.kind.isClass } - .filter { incomplete.isEmpty() || it.name.startsWith(incomplete, ignoreCase = true) } - - classesInPackage.take(30).forEach { cls -> - if (cls.name !in addedNames) { - addedNames.add(cls.name) - items.add(CompletionItem().apply { - label = cls.name - kind = cls.kind.toCompletionKind() - insertText = cls.name - detail = cls.fqName - sortText = "1_${cls.name}" - }) - } - } - } - - return items - } - - private fun provideStatementCompletions( - prefix: String, - symbolTable: SymbolTable?, - analysisContext: AnalysisContext?, - line: Int, - character: Int - ): List { - val items = mutableListOf() - val position = Position(line, character) - - val importedPackages = symbolTable?.imports - ?.map { it.packageName } - ?.filter { it.isNotEmpty() } - ?.toSet() - ?: emptySet() - - val visibleSymbols = symbolTable?.allVisibleSymbols(position) ?: emptyList() - visibleSymbols - .filter { prefix.isEmpty() || it.name.startsWith(prefix, ignoreCase = true) } - .forEach { symbol -> - val priority = when (symbol) { - is ParameterSymbol, is PropertySymbol -> CompletionItemPriority.LOCAL - else -> CompletionItemPriority.MEMBER - } - items.add(createCompletionItem(symbol, priority, analysisContext)) - } - - if (prefix.isEmpty()) { - return items - } - - val classpathIndex = projectIndex.getClasspathIndex() - val classpathSymbols = classpathIndex?.findByPrefix(prefix, 30) ?: emptyList() - val classpathFqNames = classpathSymbols.map { it.fqName }.toSet() - - val projectSymbols = projectIndex.findByPrefix(prefix, limit = 20) - .filter { it.fqName !in classpathFqNames } - - val stdlibSymbols = projectIndex.getStdlibIndex()?.findByPrefix(prefix, 20) ?: emptyList() - - val sortedProjectSymbols = projectSymbols.sortedWith( - compareBy { if (it.name.equals(prefix, ignoreCase = true)) 0 else 1 } - .thenBy { it.name } - ) - - val sortedClasspathSymbols = classpathSymbols.sortedWith( - compareBy { if (it.packageName in importedPackages) 0 else 1 } - .thenBy { if (it.name.equals(prefix, ignoreCase = true)) 0 else 1 } - .thenBy { it.name } - ) - - val sortedStdlibSymbols = stdlibSymbols.sortedWith( - compareBy { if (it.packageName in importedPackages) 0 else 1 } - .thenBy { if (it.name.equals(prefix, ignoreCase = true)) 0 else 1 } - .thenBy { it.name } - ) - - val addedNames = items.map { it.label }.toMutableSet() - - sortedProjectSymbols.take(30).forEach { indexed -> - if (indexed.name !in addedNames) { - addedNames.add(indexed.name) - items.add(createCompletionItemFromIndexed(indexed, CompletionItemPriority.LOCAL, importedPackages)) - } - } - - sortedClasspathSymbols.take(50).forEach { indexed -> - if (indexed.name !in addedNames) { - addedNames.add(indexed.name) - items.add(createCompletionItemFromIndexed(indexed, CompletionItemPriority.IMPORTED, importedPackages)) - } - } - - sortedStdlibSymbols.take(30).forEach { indexed -> - if (indexed.name !in addedNames) { - addedNames.add(indexed.name) - items.add(createCompletionItemFromIndexed(indexed, CompletionItemPriority.STDLIB, importedPackages)) - } - } - - if (prefix.isNotEmpty()) { - KOTLIN_KEYWORDS - .filter { it.startsWith(prefix, ignoreCase = true) && it !in addedNames } - .forEach { keyword -> - items.add(CompletionItem().apply { - label = keyword - kind = CompletionItemKind.Keyword - insertText = keyword - sortText = "${CompletionItemPriority.KEYWORD.ordinal}_$keyword" - }) - } - } - - return items - } - - private fun provideAnnotationCompletions(prefix: String): List { - val items = mutableListOf() - - val annotations = projectIndex.getAllClasses() - .filter { it.kind == IndexedSymbolKind.ANNOTATION_CLASS } - .filter { prefix.isEmpty() || it.name.startsWith(prefix, ignoreCase = true) } - .take(30) - - annotations.forEach { ann -> - items.add(CompletionItem().apply { - label = ann.name - kind = CompletionItemKind.Class - insertText = ann.name - detail = ann.packageName - data = ann.fqName - }) - } - - return items - } - - private fun analyzeWithCompletionPatch( - state: DocumentState, - uri: String, - cursorLine: Int - ): Pair { - val content = state.content - val patchedContent = patchDanglingDots(content, cursorLine) - - if (patchedContent != content) { - val parser = KotlinParser() - val parseResult = parser.parse(patchedContent, state.filePath) - val symbolTable = SymbolBuilder.build(parseResult.tree, state.filePath) - val analysisContext = AnalysisContext( - tree = parseResult.tree, - symbolTable = symbolTable, - filePath = state.filePath, - stdlibIndex = projectIndex.getStdlibIndex(), - projectIndex = projectIndex, - syntaxErrorRanges = parseResult.syntaxErrors.map { it.range } - ) - return symbolTable to analysisContext - } - - analysisScheduler.analyzeSync(uri) - return state.symbolTable to state.analysisContext - } - - private fun patchDanglingDots(content: String, cursorLine: Int): String { - val lines = content.split('\n').toMutableList() - var patched = false - - if (cursorLine in lines.indices) { - val trimmed = lines[cursorLine].trimEnd() - if (trimmed.endsWith('.')) { - val nextNonBlank = ((cursorLine + 1) until lines.size) - .firstOrNull { lines[it].isNotBlank() } - ?.let { lines[it].trimStart() } - val isContinuation = nextNonBlank != null && - nextNonBlank.isNotEmpty() && - (nextNonBlank[0].isLetterOrDigit() || nextNonBlank[0] == '_' || nextNonBlank[0] == '.') - - if (!isContinuation) { - val dotPos = lines[cursorLine].lastIndexOf('.') - val beforeDot = lines[cursorLine].substring(0, dotPos).trimEnd() - if (beforeDot.isNotEmpty() && (beforeDot.last().isLetterOrDigit() || beforeDot.last() == '_' || beforeDot.last() == ')' || beforeDot.last() == ']' || beforeDot.last() == '?')) { - lines[cursorLine] = lines[cursorLine].substring(0, dotPos + 1) + "toString()" - patched = true - } - } - } - } - - return if (patched) lines.joinToString("\n") else content - } - - private fun resolveToFqName(typeName: String, symbolTable: SymbolTable?): String { - if (typeName.contains('.')) return typeName - - symbolTable?.imports?.forEach { import -> - if (!import.isStar) { - val importedName = import.alias ?: import.fqName.substringAfterLast('.') - if (importedName == typeName) return import.fqName - } - } - - if (symbolTable != null) { - val pkg = symbolTable.packageName - val hasDeclaredType = symbolTable.classes.any { it.name == typeName } - || symbolTable.typeAliases.any { it.name == typeName } - if (hasDeclaredType) { - return if (pkg.isNotEmpty()) "$pkg.$typeName" else typeName - } - } - - val projectMatches = projectIndex.findInProjectFiles(typeName).filter { it.kind.isClass } - if (projectMatches.isNotEmpty()) { - if (projectMatches.size == 1) return projectMatches.first().fqName - val samePackage = projectMatches.firstOrNull { it.packageName == symbolTable?.packageName } - return (samePackage ?: projectMatches.first()).fqName - } - - symbolTable?.imports?.filter { it.isStar }?.forEach { import -> - val candidate = "${import.fqName}.$typeName" - if (projectIndex.findByFqName(candidate) != null) return candidate - } - - val classpathIndex = projectIndex.getClasspathIndex() - if (classpathIndex != null) { - val matches = classpathIndex.findBySimpleName(typeName).filter { it.kind.isClass } - if (matches.size == 1) return matches.first().fqName - } - - val stdlibIndex = projectIndex.getStdlibIndex() - if (stdlibIndex != null) { - val stdlibMatch = stdlibIndex.findBySimpleName(typeName).firstOrNull { it.kind.isClass } - if (stdlibMatch != null) return stdlibMatch.fqName - } - - return typeName - } - - private fun findStdlibMembersWithInheritance( - simpleTypeName: String, - stdlibIndex: StdlibIndex - ): List { - val visited = mutableSetOf() - val result = mutableListOf() - val queue = ArrayDeque() - queue.add(simpleTypeName) - - while (queue.isNotEmpty()) { - val current = queue.removeFirst() - if (!visited.add(current)) continue - - val fqCandidates = stdlibIndex.findBySimpleName(current).filter { it.kind.isClass } - for (classSymbol in fqCandidates) { - result.addAll(stdlibIndex.findMembers(classSymbol.fqName)) - classSymbol.superTypes.forEach { superType -> - val superSimple = superType.substringAfterLast('.') - if (superSimple !in visited) queue.add(superSimple) - } - } - } - - val seen = mutableSetOf() - return result.filter { member -> - val key = member.kind.name + ":" + member.name + "(" + member.parameters.joinToString(",") { it.type } + ")" - seen.add(key) - } - } - - private fun findClasspathMembersWithInheritance( - classFqName: String, - classpathIndex: ClasspathIndex - ): List { - val visited = mutableSetOf() - val result = mutableListOf() - val queue = ArrayDeque() - queue.add(classFqName) - - while (queue.isNotEmpty()) { - val current = queue.removeFirst() - if (!visited.add(current)) continue - - result.addAll(classpathIndex.findMembers(current)) - - val classSymbol = classpathIndex.findByFqName(current) - classSymbol?.superTypes?.forEach { superType -> - if (superType !in visited) queue.add(superType) - } - } - - val seen = mutableSetOf() - return result.filter { member -> - val key = member.kind.name + ":" + member.name + "(" + member.parameters.joinToString(",") { it.type } + ")" - seen.add(key) - } - } - - private fun resolveReceiverType( - receiver: String, - symbolTable: SymbolTable?, - analysisContext: AnalysisContext?, - line: Int, - character: Int - ): TypeReference? { - if (symbolTable == null) return null - - val cleanReceiver = receiver.trimEnd('?') - if (cleanReceiver.isEmpty()) return null - - val position = Position(line, character) - - if (cleanReceiver == "super") { - val enclosingClass = findEnclosingClassSymbol(symbolTable, position) - if (enclosingClass != null && enclosingClass.superTypes.isNotEmpty()) { - return enclosingClass.superTypes.first() - } - return null - } - - if (cleanReceiver.endsWith(")")) { - val result = resolveCallExpressionType(cleanReceiver, symbolTable, analysisContext, line, character) - if (result != null) return result - } - - if (cleanReceiver.contains('.')) { - val parts = cleanReceiver.split('.') - var currentType: TypeReference? = null - for ((index, part) in parts.withIndex()) { - if (index == 0) { - currentType = resolveReceiverType(part, symbolTable, analysisContext, line, character) - } else { - if (currentType == null) return null - currentType = resolveMemberReturnType(currentType.name, part, symbolTable) - } - } - return currentType - } - - val symbols = symbolTable.resolve(cleanReceiver, position) - val symbol = symbols.firstOrNull() ?: return null - - val smartCastType = analysisContext?.getSmartCastTypeAtPosition(symbol, line, character) - if (smartCastType != null) { - return TypeReference(smartCastType.render(), emptyList()) - } - - val explicitType = when (symbol) { - is PropertySymbol -> symbol.type - is ParameterSymbol -> symbol.type - is FunctionSymbol -> symbol.returnType - else -> null - } - - if (explicitType != null) return explicitType - - val inferredType = analysisContext?.getSymbolType(symbol) - if (inferredType != null) { - return TypeReference(inferredType.render(), emptyList()) - } - - return null - } - - private fun findEnclosingClassSymbol(symbolTable: SymbolTable, position: Position): ClassSymbol? { - var scope = symbolTable.scopeAt(position) - while (scope != null) { - val owner = scope.owner - if (owner is ClassSymbol) return owner - scope = scope.parent - } - return null - } - - private fun resolveCallExpressionType( - expression: String, - symbolTable: SymbolTable?, - analysisContext: AnalysisContext?, - line: Int, - character: Int - ): TypeReference? { - val openParen = findMatchingOpenParen(expression) - if (openParen <= 0) return null - - val funcPart = expression.substring(0, openParen) - val lastDot = funcPart.lastIndexOf('.') - if (lastDot >= 0) { - val objReceiver = funcPart.substring(0, lastDot) - val methodName = funcPart.substring(lastDot + 1) - val objType = resolveReceiverType(objReceiver, symbolTable, analysisContext, line, character) - if (objType != null) { - return resolveMemberReturnType(objType.name, methodName, symbolTable) - } - } else { - val position = Position(line, character) - val symbols = symbolTable?.resolve(funcPart, position) ?: emptyList() - val func = symbols.firstOrNull() - if (func is FunctionSymbol && func.returnType != null) return func.returnType - if (func is ClassSymbol) return TypeReference(func.name) - val fqName = resolveToFqName(funcPart, symbolTable) - val cpClass = projectIndex.getClasspathIndex()?.findByFqName(fqName) - if (cpClass != null && cpClass.kind.isClass) return TypeReference(cpClass.fqName) - } - return null - } - - private fun findMatchingOpenParen(expression: String): Int { - var depth = 0 - for (i in expression.length - 1 downTo 0) { - when (expression[i]) { - ')' -> depth++ - '(' -> { - depth-- - if (depth == 0) return i - } - } - } - return -1 - } - - private fun resolveMemberReturnType( - typeName: String, - memberName: String, - symbolTable: SymbolTable? - ): TypeReference? { - val fqName = resolveToFqName(typeName, symbolTable) - val visited = mutableSetOf() - val queue = ArrayDeque() - queue.add(fqName) - - val classpathIndex = projectIndex.getClasspathIndex() - val stdlibIndex = projectIndex.getStdlibIndex() - - val getterName = "get${memberName.replaceFirstChar { c -> c.uppercase() }}" - - while (queue.isNotEmpty()) { - val current = queue.removeFirst() - if (!visited.add(current)) continue - - if (symbolTable != null) { - val classSymbol = symbolTable.classes.firstOrNull { it.qualifiedName == current } - if (classSymbol != null) { - val localMembers = classSymbol.findMember(memberName) + classSymbol.findMember(getterName) - for (member in localMembers) { - val returnType = when (member) { - is FunctionSymbol -> member.returnType - is PropertySymbol -> member.type - else -> null - } - if (returnType != null) return returnType - } - classSymbol.superTypes.forEach { st -> - val stName = st.render() - if (stName !in visited) queue.add(stName) - } - } - } - - val projectMembers = projectIndex.findMembersInProjectFiles(current) - val projectMatch = projectMembers.firstOrNull { it.name == memberName } - ?: projectMembers.firstOrNull { it.name == getterName } - if (projectMatch?.returnType != null) return TypeReference(projectMatch.returnType) - - projectIndex.findByFqNameInProjectFiles(current)?.superTypes?.forEach { st -> - if (st !in visited) queue.add(st) - } - - if (classpathIndex != null) { - val members = classpathIndex.findMembers(current) - val match = members.firstOrNull { it.name == memberName } - ?: members.firstOrNull { it.name == getterName } - if (match?.returnType != null) return TypeReference(match.returnType) - - classpathIndex.findByFqName(current)?.superTypes?.forEach { st -> - if (st !in visited) queue.add(st) - } - } - - if (stdlibIndex != null) { - val stdMembers = stdlibIndex.findMembers(current) - val stdMatch = stdMembers.firstOrNull { it.name == memberName } - ?: stdMembers.firstOrNull { it.name == getterName } - if (stdMatch?.returnType != null) return TypeReference(stdMatch.returnType) - - stdlibIndex.findByFqName(current)?.superTypes?.forEach { st -> - if (st !in visited) queue.add(st) - } - } - } - - if (stdlibIndex != null) { - val simpleName = fqName.substringAfterLast('.') - val candidates = stdlibIndex.findBySimpleName(simpleName).filter { it.kind.isClass } - for (cls in candidates) { - val members = stdlibIndex.findMembers(cls.fqName) - val match = members.firstOrNull { it.name == memberName } - ?: members.firstOrNull { it.name == "get${memberName.replaceFirstChar { c -> c.uppercase() }}" } - if (match?.returnType != null) return TypeReference(match.returnType) - } - } - - return null - } - - private fun isFilteredMember(name: String): Boolean { - if (name == "" || name == "") return true - if (name.startsWith("access$") || name.startsWith("$")) return true - return false - } - - private fun addInheritedMembers( - classSymbol: ClassSymbol, - symbolTable: SymbolTable, - analysisContext: AnalysisContext?, - position: Position, - items: MutableList, - addedMemberNames: MutableSet - ) { - if (classSymbol.superTypes.isEmpty()) return - - val cpIndex = projectIndex.getClasspathIndex() - val stIndex = projectIndex.getStdlibIndex() - val superTypeQueue = ArrayDeque(classSymbol.superTypes.map { it.name }) - val visitedSupers = mutableSetOf() - - while (superTypeQueue.isNotEmpty()) { - val superName = superTypeQueue.removeFirst() - if (!visitedSupers.add(superName)) continue - - val superSymbol = symbolTable.resolve(superName, position).firstOrNull() - if (superSymbol is ClassSymbol) { - superSymbol.members.forEach { member -> - if (member.name !in addedMemberNames) { - addedMemberNames.add(member.name) - items.add(createCompletionItem(member, CompletionItemPriority.MEMBER, analysisContext)) - } - } - superSymbol.superTypes.forEach { superTypeQueue.add(it.name) } - } - - val superFqName = resolveToFqName(superName, symbolTable) - if (cpIndex != null) { - findClasspathMembersWithInheritance(superFqName, cpIndex).forEach { member -> - if (member.name !in addedMemberNames && !isFilteredMember(member.name)) { - addedMemberNames.add(member.name) - items.add(createCompletionItemFromIndexed(member, CompletionItemPriority.MEMBER)) - } - } - } - if (stIndex != null) { - findStdlibMembersWithInheritance(superFqName.substringAfterLast('.'), stIndex).forEach { member -> - if (member.name !in addedMemberNames) { - addedMemberNames.add(member.name) - items.add(createCompletionItemFromIndexed(member, CompletionItemPriority.MEMBER)) - } - } - } - } - } - - private fun createCompletionItem( - symbol: Symbol, - priority: CompletionItemPriority, - analysisContext: AnalysisContext? = null - ): CompletionItem { - return CompletionItem().apply { - label = symbol.name - kind = symbol.toCompletionKind() - sortText = "${priority.ordinal}_${symbol.name}" - - when (symbol) { - is FunctionSymbol -> { - detail = symbol.toSignatureString() - insertText = if (symbol.parameters.isEmpty()) { - "${symbol.name}()" - } else { - symbol.name - } - insertTextFormat = InsertTextFormat.PlainText - } - is PropertySymbol -> { - val inferredType = analysisContext?.getSymbolType(symbol) - detail = symbol.type?.render() - ?: inferredType?.render() - ?: "Unknown" - } - is ParameterSymbol -> { - val inferredType = analysisContext?.getSymbolType(symbol) - detail = symbol.type?.render() - ?: inferredType?.render() - ?: "Unknown" - } - is ClassSymbol -> { - detail = symbol.kind.name.lowercase() - } - else -> {} - } - } - } - - private fun createCompletionItemFromIndexed( - symbol: IndexedSymbol, - priority: CompletionItemPriority, - importedPackages: Set = emptySet() - ): CompletionItem { - val importBoost = if (symbol.packageName in importedPackages) "0" else "1" - return CompletionItem().apply { - label = symbol.name - kind = symbol.kind.toCompletionKind() - sortText = "${priority.ordinal}${importBoost}_${symbol.name}" - detail = when { - symbol.kind.isCallable -> symbol.toDisplayString() - else -> symbol.packageName - } - data = symbol.fqName - - if (symbol.deprecated) { - @Suppress("DEPRECATION") - deprecated = true - tags = listOf(CompletionItemTag.Deprecated) - } - - if (symbol.kind == IndexedSymbolKind.FUNCTION) { - insertText = if (symbol.arity == 0) "${symbol.name}()" else symbol.name - } - } - } - - private fun buildDocumentation(symbol: IndexedSymbol): String { - return buildString { - append("```kotlin\n") - append(symbol.toDisplayString()) - append("\n```\n\n") - append("**Package:** ${symbol.packageName}") - if (symbol.deprecated) { - append("\n\n⚠️ **Deprecated**") - symbol.deprecationMessage?.let { append(": $it") } - } - } - } - - private fun findCallContext(content: String, offset: Int): CallContext? { - var parenDepth = 0 - var argIndex = 0 - var functionStart = -1 - - for (i in (offset - 1) downTo 0) { - when (content[i]) { - ')' -> parenDepth++ - '(' -> { - if (parenDepth == 0) { - functionStart = i - break - } - parenDepth-- - } - ',' -> { - if (parenDepth == 0) argIndex++ - } - } - } - - if (functionStart < 0) return null - - var nameEnd = functionStart - 1 - while (nameEnd >= 0 && content[nameEnd].isWhitespace()) nameEnd-- - - var nameStart = nameEnd - while (nameStart > 0 && (content[nameStart - 1].isLetterOrDigit() || content[nameStart - 1] == '_')) { - nameStart-- - } - - if (nameStart > nameEnd) return null - - val functionName = content.substring(nameStart, nameEnd + 1) - return CallContext(functionName, argIndex) - } - - private fun buildSignatureLabel(func: IndexedSymbol): String { - return buildString { - append(func.name) - append("(") - append(func.parameters.joinToString(", ") { param -> - "${param.name}: ${param.type}" - }) - append(")") - func.returnType?.let { append(": $it") } - } - } - - companion object { - private val KOTLIN_KEYWORDS = listOf( - "abstract", "actual", "annotation", "as", "break", "by", "catch", - "class", "companion", "const", "constructor", "continue", "crossinline", - "data", "do", "else", "enum", "expect", "external", "false", "final", - "finally", "for", "fun", "get", "if", "import", "in", "infix", "init", - "inline", "inner", "interface", "internal", "is", "lateinit", "noinline", - "null", "object", "open", "operator", "out", "override", "package", - "private", "protected", "public", "reified", "return", "sealed", "set", - "super", "suspend", "tailrec", "this", "throw", "true", "try", "typealias", - "val", "var", "vararg", "when", "where", "while" - ) - } -} - -private sealed class CompletionKind { - data class MemberAccess(val receiver: String, val prefix: String) : CompletionKind() - data class TypeAnnotation(val prefix: String) : CompletionKind() - data class Import(val prefix: String) : CompletionKind() - data class Statement(val prefix: String) : CompletionKind() - data class Annotation(val prefix: String) : CompletionKind() -} - -private enum class CompletionItemPriority { - LOCAL, - MEMBER, - EXTENSION, - IMPORTED, - STDLIB, - KEYWORD -} - -private data class CallContext( - val functionName: String, - val argumentIndex: Int -) - -private fun Symbol.toCompletionKind(): CompletionItemKind { - return when (this) { - is FunctionSymbol -> CompletionItemKind.Function - is PropertySymbol -> { - val scopeKind = containingScope?.kind - if (scopeKind != null && scopeKind.canContainLocals) { - CompletionItemKind.Variable - } else { - CompletionItemKind.Property - } - } - is ParameterSymbol -> CompletionItemKind.Variable - is ClassSymbol -> when (kind) { - ClassKind.INTERFACE -> CompletionItemKind.Interface - ClassKind.ENUM_CLASS -> CompletionItemKind.Enum - ClassKind.OBJECT, ClassKind.COMPANION_OBJECT -> CompletionItemKind.Module - else -> CompletionItemKind.Class - } - is TypeParameterSymbol -> CompletionItemKind.TypeParameter - else -> CompletionItemKind.Text - } -} - -private fun IndexedSymbolKind.toCompletionKind(): CompletionItemKind { - return when (this) { - IndexedSymbolKind.CLASS -> CompletionItemKind.Class - IndexedSymbolKind.INTERFACE -> CompletionItemKind.Interface - IndexedSymbolKind.OBJECT -> CompletionItemKind.Module - IndexedSymbolKind.ENUM_CLASS -> CompletionItemKind.Enum - IndexedSymbolKind.ANNOTATION_CLASS -> CompletionItemKind.Class - IndexedSymbolKind.DATA_CLASS -> CompletionItemKind.Class - IndexedSymbolKind.VALUE_CLASS -> CompletionItemKind.Class - IndexedSymbolKind.FUNCTION -> CompletionItemKind.Function - IndexedSymbolKind.PROPERTY -> CompletionItemKind.Property - IndexedSymbolKind.CONSTRUCTOR -> CompletionItemKind.Constructor - IndexedSymbolKind.TYPE_ALIAS -> CompletionItemKind.TypeParameter - } -} - -private fun FunctionSymbol.toSignatureString(): String { - return buildString { - append("fun ") - if (typeParameters.isNotEmpty()) { - append("<") - append(typeParameters.joinToString(", ") { it.name }) - append("> ") - } - receiverType?.let { append("${it.render()}.") } - append(name) - append("(") - append(parameters.joinToString(", ") { "${it.name}: ${it.type?.render() ?: "Any"}" }) - append(")") - returnType?.let { append(": ${it.render()}") } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DefinitionProvider.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DefinitionProvider.kt deleted file mode 100644 index a9857fc594..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DefinitionProvider.kt +++ /dev/null @@ -1,319 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server.providers - -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbol -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.parser.Position -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxKind -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxNode -import org.appdevforall.codeonthego.lsp.kotlin.server.AnalysisScheduler -import org.appdevforall.codeonthego.lsp.kotlin.server.DocumentManager -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable -import org.eclipse.lsp4j.* - -/** - * Provides go-to-definition and find-references functionality. - */ -class DefinitionProvider( - private val documentManager: DocumentManager, - private val projectIndex: ProjectIndex, - private val analysisScheduler: AnalysisScheduler -) { - fun provideDefinition(uri: String, line: Int, character: Int): List { - val state = documentManager.get(uri) ?: return emptyList() - - analysisScheduler.analyzeSync(uri) - - val syntaxTree = state.syntaxTree ?: return emptyList() - val symbolTable = state.symbolTable - - val node = findNodeAtPosition(syntaxTree.root, line, character) ?: return emptyList() - - if (node.kind != SyntaxKind.SIMPLE_IDENTIFIER) { - return emptyList() - } - - val name = node.text - val locations = mutableListOf() - - val position = Position(line, character) - val symbols = symbolTable?.resolve(name, position) ?: emptyList() - val localSymbol = symbols.firstOrNull() - if (localSymbol != null) { - val loc = symbolToLocation(localSymbol) - if (loc != null) { - locations.add(loc) - } - } - - if (locations.isEmpty()) { - val indexedSymbol = projectIndex.findByFqName(name) - ?: projectIndex.findByPrefix(name, limit = 10).find { it.name == name } - - if (indexedSymbol != null) { - val loc = indexedSymbolToLocation(indexedSymbol) - if (loc != null) { - locations.add(loc) - } - } - } - - return locations - } - - fun provideReferences( - uri: String, - line: Int, - character: Int, - includeDeclaration: Boolean - ): List { - val state = documentManager.get(uri) ?: return emptyList() - - analysisScheduler.analyzeSync(uri) - - val syntaxTree = state.syntaxTree ?: return emptyList() - val symbolTable = state.symbolTable - - val node = findNodeAtPosition(syntaxTree.root, line, character) ?: return emptyList() - - if (node.kind != SyntaxKind.SIMPLE_IDENTIFIER) { - return emptyList() - } - - val name = node.text - val references = mutableListOf() - - val position = Position(line, character) - val symbols = symbolTable?.resolve(name, position) ?: emptyList() - val targetSymbol = symbols.firstOrNull() - - val targetFqName = when { - targetSymbol != null -> getFqName(targetSymbol) - else -> { - val indexed = projectIndex.findByPrefix(name, limit = 1).find { it.name == name } - indexed?.fqName - } - } - - if (targetFqName != null) { - if (includeDeclaration && targetSymbol != null) { - val declLoc = symbolToLocation(targetSymbol) - if (declLoc != null) { - references.add(declLoc) - } - } - - findReferencesInDocumentWithFqName(syntaxTree.root, name, targetFqName, symbolTable, uri, references) - - for (otherUri in documentManager.openUris) { - if (otherUri == uri) continue - - val otherState = documentManager.get(otherUri) ?: continue - val otherTree = otherState.syntaxTree ?: continue - val otherSymbolTable = otherState.symbolTable - - findReferencesInDocumentWithFqName(otherTree.root, name, targetFqName, otherSymbolTable, otherUri, references) - } - - val indexedReferences = projectIndex.findSymbolReferences(targetFqName) - for (ref in indexedReferences) { - val fileIndex = projectIndex.getFileIndex(ref.filePath) - val refSymbol = fileIndex?.findByFqName(ref.referenceFqName) - if (refSymbol != null && refSymbol.hasLocation) { - references.add(Location( - ref.filePath, - Range( - org.eclipse.lsp4j.Position(refSymbol.startLine ?: 0, refSymbol.startColumn ?: 0), - org.eclipse.lsp4j.Position(refSymbol.endLine ?: 0, refSymbol.endColumn ?: 0) - ) - )) - } - } - } - - return references.distinctBy { "${it.uri}:${it.range.start.line}:${it.range.start.character}" } - } - - fun provideHighlights(uri: String, line: Int, character: Int): List { - val state = documentManager.get(uri) ?: return emptyList() - - analysisScheduler.analyzeSync(uri) - - val syntaxTree = state.syntaxTree ?: return emptyList() - val symbolTable = state.symbolTable - - val node = findNodeAtPosition(syntaxTree.root, line, character) ?: return emptyList() - - if (node.kind != SyntaxKind.SIMPLE_IDENTIFIER) { - return emptyList() - } - - val name = node.text - val highlights = mutableListOf() - - val position = Position(line, character) - val symbols = symbolTable?.resolve(name, position) ?: emptyList() - val targetSymbol = symbols.firstOrNull() - - collectHighlights(syntaxTree.root, name, targetSymbol, highlights) - - return highlights - } - - private fun findNodeAtPosition(root: SyntaxNode, line: Int, character: Int): SyntaxNode? { - fun search(node: SyntaxNode): SyntaxNode? { - if (!containsPosition(node, line, character)) return null - - for (child in node.children) { - val found = search(child) - if (found != null) return found - } - - return node - } - return search(root) - } - - private fun containsPosition(node: SyntaxNode, line: Int, character: Int): Boolean { - if (line < node.startLine || line > node.endLine) return false - if (line == node.startLine && character < node.startColumn) return false - if (line == node.endLine && character > node.endColumn) return false - return true - } - - private fun symbolToLocation(symbol: Symbol): Location? { - val location = symbol.location - if (location.isSynthetic) return null - return Location( - location.filePath, - Range( - org.eclipse.lsp4j.Position(location.range.startLine, location.range.startColumn), - org.eclipse.lsp4j.Position(location.range.endLine, location.range.endColumn) - ) - ) - } - - private fun indexedSymbolToLocation(symbol: IndexedSymbol): Location? { - val filePath = symbol.filePath ?: return null - - val startLine = symbol.startLine ?: 0 - val startColumn = symbol.startColumn ?: 0 - val endLine = symbol.endLine ?: startLine - val endColumn = symbol.endColumn ?: startColumn - - return Location( - filePath, - Range( - org.eclipse.lsp4j.Position(startLine, startColumn), - org.eclipse.lsp4j.Position(endLine, endColumn) - ) - ) - } - - private fun findReferencesInDocumentWithFqName( - root: SyntaxNode, - name: String, - targetFqName: String, - symbolTable: SymbolTable?, - uri: String, - references: MutableList - ) { - fun traverse(node: SyntaxNode) { - if (node.kind == SyntaxKind.SIMPLE_IDENTIFIER && node.text == name) { - var isMatch = false - - if (symbolTable != null) { - val position = Position(node.startLine, node.startColumn) - val resolved = symbolTable.resolve(name, position) - val symbol = resolved.firstOrNull() - if (symbol != null && symbol.qualifiedName == targetFqName) { - isMatch = true - } - } else { - isMatch = true - } - - if (isMatch) { - references.add(Location( - uri, - Range( - org.eclipse.lsp4j.Position(node.startLine, node.startColumn), - org.eclipse.lsp4j.Position(node.endLine, node.endColumn) - ) - )) - } - } - - for (child in node.children) { - traverse(child) - } - } - traverse(root) - } - - private fun collectHighlights( - root: SyntaxNode, - name: String, - targetSymbol: Symbol?, - highlights: MutableList - ) { - fun traverse(node: SyntaxNode) { - if (node.kind == SyntaxKind.SIMPLE_IDENTIFIER) { - val nodeText = node.text - if (nodeText == name) { - val kind = determineHighlightKind(node, targetSymbol) - highlights.add(DocumentHighlight( - Range( - org.eclipse.lsp4j.Position(node.startLine, node.startColumn), - org.eclipse.lsp4j.Position(node.endLine, node.endColumn) - ), - kind - )) - } - } - - for (child in node.children) { - traverse(child) - } - } - traverse(root) - } - - private fun determineHighlightKind(node: SyntaxNode, targetSymbol: Symbol?): DocumentHighlightKind { - val parent = node.parent - - if (parent != null) { - when (parent.kind) { - SyntaxKind.FUNCTION_DECLARATION, - SyntaxKind.CLASS_DECLARATION, - SyntaxKind.PROPERTY_DECLARATION, - SyntaxKind.PARAMETER -> { - val nameChild = parent.childByFieldName("name") - if (nameChild != null && - nameChild.startLine == node.startLine && - nameChild.startColumn == node.startColumn) { - return DocumentHighlightKind.Write - } - } - SyntaxKind.ASSIGNMENT, - SyntaxKind.AUGMENTED_ASSIGNMENT -> { - val left = parent.childByFieldName("left") - if (left != null && containsSamePosition(left, node)) { - return DocumentHighlightKind.Write - } - } - else -> {} - } - } - - return DocumentHighlightKind.Read - } - - private fun containsSamePosition(a: SyntaxNode, b: SyntaxNode): Boolean { - return a.startLine == b.startLine && a.startColumn == b.startColumn - } - - private fun getFqName(symbol: Symbol): String { - return symbol.qualifiedName - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DiagnosticProvider.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DiagnosticProvider.kt deleted file mode 100644 index 1b3aed9444..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DiagnosticProvider.kt +++ /dev/null @@ -1,120 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server.providers - -import org.appdevforall.codeonthego.lsp.kotlin.parser.TextRange -import org.appdevforall.codeonthego.lsp.kotlin.semantic.Diagnostic -import org.appdevforall.codeonthego.lsp.kotlin.semantic.DiagnosticSeverity -import org.appdevforall.codeonthego.lsp.kotlin.server.AnalysisScheduler -import org.appdevforall.codeonthego.lsp.kotlin.server.DocumentManager -import org.appdevforall.codeonthego.lsp.kotlin.server.toLsp4j -import org.eclipse.lsp4j.Position -import org.eclipse.lsp4j.PublishDiagnosticsParams -import org.eclipse.lsp4j.Range -import org.eclipse.lsp4j.services.LanguageClient - -/** - * Provides diagnostic publishing for errors and warnings. - * - * DiagnosticProvider converts internal diagnostics to LSP format - * and publishes them to the client. - * - * ## Diagnostic Lifecycle - * - * 1. Document is edited - * 2. AnalysisScheduler runs analysis - * 3. SemanticAnalyzer produces diagnostics - * 4. DiagnosticProvider converts and publishes to client - * - * ## Diagnostic Types - * - * - **Error**: Syntax errors, type mismatches, unresolved references - * - **Warning**: Unused variables, deprecated API usage - * - **Info**: Code style suggestions - * - **Hint**: Refactoring opportunities - */ -class DiagnosticProvider( - private val documentManager: DocumentManager, - private val analysisScheduler: AnalysisScheduler -) { - @Volatile - private var client: LanguageClient? = null - - fun setClient(client: LanguageClient) { - this.client = client - } - - fun publishDiagnostics(uri: String, diagnostics: List) { - val lspDiagnostics = diagnostics.map { it.toLsp4j() } - - client?.publishDiagnostics(PublishDiagnosticsParams( - uri, - lspDiagnostics - )) - } - - fun publishDiagnosticsWithVersion(uri: String, version: Int, diagnostics: List) { - val lspDiagnostics = diagnostics.map { it.toLsp4j() } - - client?.publishDiagnostics(PublishDiagnosticsParams( - uri, - lspDiagnostics, - version - )) - } - - fun clearDiagnostics(uri: String) { - client?.publishDiagnostics(PublishDiagnosticsParams( - uri, - emptyList() - )) - } - - fun getDiagnostics(uri: String): List { - val state = documentManager.get(uri) ?: return emptyList() - - analysisScheduler.analyzeSync(uri) - - return state.diagnostics - } - - fun getDiagnosticsInRange(uri: String, range: Range): List { - return getDiagnostics(uri).filter { diagnostic -> - rangesOverlap(diagnostic.range.toLspRange(), range) - } - } - - fun getErrorCount(uri: String): Int { - return getDiagnostics(uri).count { it.severity == DiagnosticSeverity.ERROR } - } - - fun getWarningCount(uri: String): Int { - return getDiagnostics(uri).count { it.severity == DiagnosticSeverity.WARNING } - } - - fun hasErrors(uri: String): Boolean { - return getDiagnostics(uri).any { it.severity == DiagnosticSeverity.ERROR } - } - - fun refreshAllDiagnostics() { - for (uri in documentManager.openUris) { - val state = documentManager.get(uri) ?: continue - publishDiagnosticsWithVersion(uri, state.version, state.diagnostics) - } - } - - private fun rangesOverlap(a: Range, b: Range): Boolean { - if (a.end.line < b.start.line) return false - if (a.start.line > b.end.line) return false - - if (a.end.line == b.start.line && a.end.character < b.start.character) return false - if (a.start.line == b.end.line && a.start.character > b.end.character) return false - - return true - } -} - -private fun TextRange.toLspRange(): Range { - return Range( - Position(startLine, startColumn), - Position(endLine, endColumn) - ) -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DocumentSymbolProvider.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DocumentSymbolProvider.kt deleted file mode 100644 index ac5f2ba000..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/DocumentSymbolProvider.kt +++ /dev/null @@ -1,154 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server.providers - -import org.appdevforall.codeonthego.lsp.kotlin.server.AnalysisScheduler -import org.appdevforall.codeonthego.lsp.kotlin.server.DocumentManager -import org.appdevforall.codeonthego.lsp.kotlin.symbol.* -import org.eclipse.lsp4j.DocumentSymbol -import org.eclipse.lsp4j.Position -import org.eclipse.lsp4j.Range -import org.eclipse.lsp4j.SymbolKind - -/** - * Provides document outline/symbol information. - * - * DocumentSymbolProvider generates a hierarchical tree of symbols - * in a document for the outline view. - * - * ## Symbol Hierarchy - * - * ``` - * Package - * └── Class - * ├── Property - * ├── Function - * └── Nested Class - * └── ... - * ``` - */ -class DocumentSymbolProvider( - private val documentManager: DocumentManager, - private val analysisScheduler: AnalysisScheduler -) { - fun provideDocumentSymbols(uri: String): List { - val state = documentManager.get(uri) ?: return emptyList() - - analysisScheduler.analyzeSync(uri) - - val symbolTable = state.symbolTable ?: return emptyList() - - return buildSymbolHierarchy(symbolTable) - } - - private fun buildSymbolHierarchy(symbolTable: SymbolTable): List { - val symbols = mutableListOf() - - if (symbolTable.packageName.isNotEmpty()) { - val packageSymbol = DocumentSymbol().apply { - name = symbolTable.packageName - kind = SymbolKind.Package - range = Range(Position(0, 0), Position(0, 0)) - selectionRange = range - children = emptyList() - } - symbols.add(packageSymbol) - } - - for (symbol in symbolTable.topLevelSymbols) { - val docSymbol = convertSymbol(symbol) - if (docSymbol != null) { - symbols.add(docSymbol) - } - } - - return symbols - } - - private fun convertSymbol(symbol: Symbol): DocumentSymbol? { - val location = symbol.location ?: return null - val range = Range( - Position(location.range.startLine, location.range.startColumn), - Position(location.range.endLine, location.range.endColumn) - ) - - return when (symbol) { - is ClassSymbol -> DocumentSymbol().apply { - name = symbol.name - kind = classKindToSymbolKind(symbol.kind) - this.range = range - selectionRange = range - detail = buildClassDetail(symbol) - children = symbol.members - .mapNotNull { convertSymbol(it) } - .plus(symbol.nestedClasses.mapNotNull { convertSymbol(it) }) - } - is FunctionSymbol -> DocumentSymbol().apply { - name = symbol.name - kind = SymbolKind.Function - this.range = range - selectionRange = range - detail = buildFunctionDetail(symbol) - children = emptyList() - } - is PropertySymbol -> DocumentSymbol().apply { - name = symbol.name - kind = if (symbol.isConst) SymbolKind.Constant else SymbolKind.Property - this.range = range - selectionRange = range - detail = symbol.type?.render() - children = emptyList() - } - is TypeAliasSymbol -> DocumentSymbol().apply { - name = symbol.name - kind = SymbolKind.TypeParameter - this.range = range - selectionRange = range - detail = "typealias" - children = emptyList() - } - else -> null - } - } - - private fun classKindToSymbolKind(kind: ClassKind): SymbolKind { - return when (kind) { - ClassKind.CLASS -> SymbolKind.Class - ClassKind.INTERFACE -> SymbolKind.Interface - ClassKind.OBJECT -> SymbolKind.Object - ClassKind.COMPANION_OBJECT -> SymbolKind.Object - ClassKind.ENUM_CLASS -> SymbolKind.Enum - ClassKind.ENUM_ENTRY -> SymbolKind.EnumMember - ClassKind.ANNOTATION_CLASS -> SymbolKind.Class - ClassKind.DATA_CLASS -> SymbolKind.Class - ClassKind.VALUE_CLASS -> SymbolKind.Class - } - } - - private fun buildClassDetail(symbol: ClassSymbol): String { - return buildString { - append(symbol.kind.name.lowercase().replace('_', ' ')) - if (symbol.typeParameters.isNotEmpty()) { - append("<") - append(symbol.typeParameters.joinToString(", ") { it.name }) - append(">") - } - if (symbol.superTypes.isNotEmpty()) { - append(" : ") - append(symbol.superTypes.first().render()) - if (symbol.superTypes.size > 1) { - append(", ...") - } - } - } - } - - private fun buildFunctionDetail(symbol: FunctionSymbol): String { - return buildString { - append("(") - append(symbol.parameters.joinToString(", ") { param -> - "${param.name}: ${param.type?.render() ?: "Any"}" - }) - append(")") - symbol.returnType?.let { append(": ${it.render()}") } - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/HoverProvider.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/HoverProvider.kt deleted file mode 100644 index ffd0ce346e..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/HoverProvider.kt +++ /dev/null @@ -1,336 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server.providers - -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbol -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbolKind -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.parser.Position -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxKind -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxNode -import org.appdevforall.codeonthego.lsp.kotlin.server.AnalysisScheduler -import org.appdevforall.codeonthego.lsp.kotlin.server.DocumentManager -import org.appdevforall.codeonthego.lsp.kotlin.symbol.* -import org.eclipse.lsp4j.Hover -import org.eclipse.lsp4j.MarkupContent -import org.eclipse.lsp4j.MarkupKind -import org.eclipse.lsp4j.Range -import org.eclipse.lsp4j.jsonrpc.messages.Either - -/** - * Provides hover information for symbols. - */ -class HoverProvider( - private val documentManager: DocumentManager, - private val projectIndex: ProjectIndex, - private val analysisScheduler: AnalysisScheduler -) { - fun provideHover(uri: String, line: Int, character: Int): Hover? { - val state = documentManager.get(uri) ?: return null - - analysisScheduler.analyzeSync(uri) - - val syntaxTree = state.syntaxTree ?: return null - val symbolTable = state.symbolTable - - val node = findNodeAtPosition(syntaxTree.root, line, character) ?: return null - - val hoverContent = when { - node.kind == SyntaxKind.SIMPLE_IDENTIFIER -> { - val name = node.text - getIdentifierHover(name, symbolTable, line, character) - } - node.kind.isLiteral -> { - getLiteralHover(node) - } - node.kind.isKeyword -> { - getKeywordHover(node.kind) - } - else -> null - } - - return hoverContent?.let { content -> - Hover().apply { - contents = Either.forRight(MarkupContent().apply { - kind = MarkupKind.MARKDOWN - value = content - }) - range = Range( - org.eclipse.lsp4j.Position(node.startLine, node.startColumn), - org.eclipse.lsp4j.Position(node.endLine, node.endColumn) - ) - } - } - } - - private fun findNodeAtPosition(root: SyntaxNode, line: Int, character: Int): SyntaxNode? { - fun search(node: SyntaxNode): SyntaxNode? { - if (!containsPosition(node, line, character)) return null - - for (child in node.children) { - val found = search(child) - if (found != null) return found - } - - return node - } - return search(root) - } - - private fun containsPosition(node: SyntaxNode, line: Int, character: Int): Boolean { - if (line < node.startLine || line > node.endLine) return false - if (line == node.startLine && character < node.startColumn) return false - if (line == node.endLine && character > node.endColumn) return false - return true - } - - private fun getIdentifierHover( - name: String, - symbolTable: SymbolTable?, - line: Int, - character: Int - ): String? { - if (symbolTable != null) { - val position = Position(line, character) - val symbols = symbolTable.resolve(name, position) - val localSymbol = symbols.firstOrNull() - if (localSymbol != null) { - return formatSymbolHover(localSymbol) - } - } - - val indexedSymbol = projectIndex.findByFqName(name) - ?: projectIndex.findByPrefix(name, limit = 1).firstOrNull { it.name == name } - - return indexedSymbol?.let { formatIndexedSymbolHover(it) } - } - - private fun formatSymbolHover(symbol: Symbol): String { - return buildString { - append("```kotlin\n") - append(formatSymbolSignature(symbol)) - append("\n```") - - when (symbol) { - is FunctionSymbol -> { - if (symbol.parameters.isNotEmpty()) { - append("\n\n**Parameters:**\n") - symbol.parameters.forEach { param -> - append("- `${param.name}`: ${param.type?.render() ?: "Any"}\n") - } - } - symbol.returnType?.let { rt -> - append("\n**Returns:** `${rt.render()}`") - } - } - is PropertySymbol -> { - symbol.type?.let { type -> - append("\n\n**Type:** `${type.render()}`") - } - if (symbol.isVar) { - append("\n\n*Mutable property (var)*") - } - } - is ClassSymbol -> { - if (symbol.superTypes.isNotEmpty()) { - append("\n\n**Inherits:** ") - append(symbol.superTypes.joinToString(", ") { "`${it.render()}`" }) - } - } - else -> {} - } - } - } - - private fun formatSymbolSignature(symbol: Symbol): String { - return when (symbol) { - is FunctionSymbol -> buildString { - if (symbol.modifiers.isSuspend) append("suspend ") - if (symbol.modifiers.isInline) append("inline ") - append("fun ") - if (symbol.typeParameters.isNotEmpty()) { - append("<") - append(symbol.typeParameters.joinToString(", ") { it.name }) - append("> ") - } - symbol.receiverType?.let { append("${it.render()}.") } - append(symbol.name) - append("(") - append(symbol.parameters.joinToString(", ") { param -> - buildString { - if (param.isVararg) append("vararg ") - append("${param.name}: ${param.type?.render() ?: "Any"}") - if (param.hasDefaultValue) append(" = ...") - } - }) - append(")") - symbol.returnType?.let { append(": ${it.render()}") } - } - is PropertySymbol -> buildString { - if (symbol.isVar) append("var ") else append("val ") - append(symbol.name) - symbol.type?.let { append(": ${it.render()}") } - } - is ParameterSymbol -> buildString { - if (symbol.isVararg) append("vararg ") - append(symbol.name) - symbol.type?.let { append(": ${it.render()}") } - } - is ClassSymbol -> buildString { - when (symbol.kind) { - ClassKind.CLASS -> { - if (symbol.modifiers.isSealed) append("sealed ") - append("class ") - } - ClassKind.INTERFACE -> append("interface ") - ClassKind.OBJECT -> append("object ") - ClassKind.COMPANION_OBJECT -> append("companion object ") - ClassKind.ENUM_CLASS -> append("enum class ") - ClassKind.ENUM_ENTRY -> append("") - ClassKind.ANNOTATION_CLASS -> append("annotation class ") - ClassKind.DATA_CLASS -> append("data class ") - ClassKind.VALUE_CLASS -> append("value class ") - } - append(symbol.name) - if (symbol.typeParameters.isNotEmpty()) { - append("<") - append(symbol.typeParameters.joinToString(", ") { it.name }) - append(">") - } - } - is TypeParameterSymbol -> buildString { - append(symbol.name) - symbol.effectiveBound?.let { append(" : ${it.render()}") } - } - else -> symbol.name - } - } - - private fun formatIndexedSymbolHover(symbol: IndexedSymbol): String { - return buildString { - append("```kotlin\n") - append(symbol.toDisplayString()) - append("\n```") - - append("\n\n**Package:** `${symbol.packageName}`") - - symbol.containingClass?.let { cls -> - append("\n\n**Containing class:** `$cls`") - } - - if (symbol.deprecated) { - append("\n\n⚠️ **Deprecated**") - symbol.deprecationMessage?.let { msg -> - append(": $msg") - } - } - - if (symbol.isExtension) { - append("\n\n*Extension ${if (symbol.kind == IndexedSymbolKind.FUNCTION) "function" else "property"}*") - } - } - } - - private fun getKeywordHover(kind: SyntaxKind): String? { - val description = KEYWORD_DESCRIPTIONS[kind] ?: return null - return buildString { - append("**`${kind.name.lowercase()}`** — ") - append(description) - } - } - - private fun getLiteralHover(node: SyntaxNode): String? { - val text = node.text - return when (node.kind) { - SyntaxKind.INTEGER_LITERAL -> { - val value = parseIntegerLiteral(text) - "```kotlin\n$text\n```\n\n**Type:** `Int` or `Long`\n\n**Value:** $value" - } - SyntaxKind.REAL_LITERAL -> { - "```kotlin\n$text\n```\n\n**Type:** `Double` or `Float`" - } - SyntaxKind.STRING_LITERAL, SyntaxKind.MULTI_LINE_STRING_LITERAL -> { - "```kotlin\n$text\n```\n\n**Type:** `String`" - } - SyntaxKind.CHARACTER_LITERAL -> { - "```kotlin\n$text\n```\n\n**Type:** `Char`" - } - SyntaxKind.BOOLEAN_LITERAL -> { - "```kotlin\n$text\n```\n\n**Type:** `Boolean`" - } - SyntaxKind.NULL_LITERAL -> { - "```kotlin\nnull\n```\n\n**Type:** `Nothing?`" - } - else -> null - } - } - - private fun parseIntegerLiteral(text: String): String { - val cleaned = text.replace("_", "").removeSuffix("L").removeSuffix("l") - return try { - when { - cleaned.startsWith("0x", ignoreCase = true) -> - cleaned.substring(2).toLong(16).toString() - cleaned.startsWith("0b", ignoreCase = true) -> - cleaned.substring(2).toLong(2).toString() - else -> cleaned - } - } catch (e: NumberFormatException) { - text - } - } - - companion object { - private val KEYWORD_DESCRIPTIONS = mapOf( - SyntaxKind.FUN to "Declares a function", - SyntaxKind.VAL to "Declares a read-only property or local variable", - SyntaxKind.VAR to "Declares a mutable property or local variable", - SyntaxKind.CLASS to "Declares a class", - SyntaxKind.INTERFACE to "Declares an interface", - SyntaxKind.OBJECT to "Declares a singleton object or companion object", - SyntaxKind.IF to "Conditional expression", - SyntaxKind.ELSE to "Alternative branch of a conditional", - SyntaxKind.WHEN to "Pattern matching expression", - SyntaxKind.FOR to "Iterates over a range, array, or iterable", - SyntaxKind.WHILE to "Loop that executes while condition is true", - SyntaxKind.DO to "Loop that executes at least once", - SyntaxKind.RETURN to "Returns from a function", - SyntaxKind.BREAK to "Terminates the nearest enclosing loop", - SyntaxKind.CONTINUE to "Proceeds to the next iteration of a loop", - SyntaxKind.THROW to "Throws an exception", - SyntaxKind.TRY to "Begins a try-catch-finally block", - SyntaxKind.CATCH to "Handles an exception", - SyntaxKind.FINALLY to "Block that always executes after try/catch", - SyntaxKind.THIS to "Reference to current instance", - SyntaxKind.SUPER to "Reference to superclass", - SyntaxKind.SUSPEND to "Marks a function as suspendable for coroutines", - SyntaxKind.INLINE to "Inlines the function at call sites", - SyntaxKind.DATA to "Generates equals, hashCode, toString, copy", - SyntaxKind.SEALED to "Restricts subclass hierarchy", - SyntaxKind.OVERRIDE to "Overrides a member from a supertype", - SyntaxKind.OPEN to "Allows the class or member to be overridden", - SyntaxKind.ABSTRACT to "Declares an abstract class or member", - SyntaxKind.FINAL to "Prevents overriding", - SyntaxKind.PRIVATE to "Visible only in the containing declaration", - SyntaxKind.PROTECTED to "Visible in the class and subclasses", - SyntaxKind.INTERNAL to "Visible within the same module", - SyntaxKind.PUBLIC to "Visible everywhere", - SyntaxKind.COMPANION to "Declares a companion object", - SyntaxKind.LATEINIT to "Allows non-null property initialization to be deferred", - SyntaxKind.CONST to "Compile-time constant", - SyntaxKind.TYPEALIAS to "Creates a type alias" - ) - } -} - -private val SyntaxKind.isKeyword: Boolean - get() = name.all { it.isUpperCase() || it == '_' } && - this != SyntaxKind.SIMPLE_IDENTIFIER && - this != SyntaxKind.ERROR - -private val SyntaxKind.isLiteral: Boolean - get() = this == SyntaxKind.INTEGER_LITERAL || - this == SyntaxKind.REAL_LITERAL || - this == SyntaxKind.STRING_LITERAL || - this == SyntaxKind.MULTI_LINE_STRING_LITERAL || - this == SyntaxKind.CHARACTER_LITERAL || - this == SyntaxKind.BOOLEAN_LITERAL || - this == SyntaxKind.NULL_LITERAL diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/SemanticTokenProvider.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/SemanticTokenProvider.kt deleted file mode 100644 index 66ccd37e71..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/server/providers/SemanticTokenProvider.kt +++ /dev/null @@ -1,467 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.server.providers - -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.parser.Position -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxKind -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxNode -import org.appdevforall.codeonthego.lsp.kotlin.semantic.AnalysisContext -import org.appdevforall.codeonthego.lsp.kotlin.server.AnalysisScheduler -import org.appdevforall.codeonthego.lsp.kotlin.server.DocumentManager -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.FunctionSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.PropertySymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Symbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeAliasSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeParameterSymbol -import org.eclipse.lsp4j.SemanticTokens -import org.eclipse.lsp4j.SemanticTokensLegend - -class SemanticTokenProvider( - private val documentManager: DocumentManager, - private val projectIndex: ProjectIndex, - private val analysisScheduler: AnalysisScheduler -) { - companion object { - val TOKEN_TYPES = listOf( - "namespace", - "type", - "class", - "enum", - "interface", - "struct", - "typeParameter", - "parameter", - "variable", - "property", - "enumMember", - "event", - "function", - "method", - "macro", - "keyword", - "modifier", - "comment", - "string", - "number", - "regexp", - "operator", - "decorator" - ) - - val TOKEN_MODIFIERS = listOf( - "declaration", - "definition", - "readonly", - "static", - "deprecated", - "abstract", - "async", - "modification", - "documentation", - "defaultLibrary" - ) - - val LEGEND = SemanticTokensLegend(TOKEN_TYPES, TOKEN_MODIFIERS) - - private const val TYPE_NAMESPACE = 0 - private const val TYPE_TYPE = 1 - private const val TYPE_CLASS = 2 - private const val TYPE_ENUM = 3 - private const val TYPE_INTERFACE = 4 - private const val TYPE_STRUCT = 5 - private const val TYPE_TYPE_PARAMETER = 6 - private const val TYPE_PARAMETER = 7 - private const val TYPE_VARIABLE = 8 - private const val TYPE_PROPERTY = 9 - private const val TYPE_ENUM_MEMBER = 10 - private const val TYPE_EVENT = 11 - private const val TYPE_FUNCTION = 12 - private const val TYPE_METHOD = 13 - private const val TYPE_MACRO = 14 - private const val TYPE_KEYWORD = 15 - private const val TYPE_MODIFIER = 16 - private const val TYPE_COMMENT = 17 - private const val TYPE_STRING = 18 - private const val TYPE_NUMBER = 19 - private const val TYPE_REGEXP = 20 - private const val TYPE_OPERATOR = 21 - private const val TYPE_DECORATOR = 22 - - private const val MOD_DECLARATION = 1 shl 0 - private const val MOD_DEFINITION = 1 shl 1 - private const val MOD_READONLY = 1 shl 2 - private const val MOD_STATIC = 1 shl 3 - private const val MOD_DEPRECATED = 1 shl 4 - private const val MOD_ABSTRACT = 1 shl 5 - - private val KOTLIN_KEYWORDS = setOf( - "as", "break", "class", "continue", "do", "else", "false", "for", "fun", - "if", "in", "interface", "is", "null", "object", "package", "return", - "super", "this", "throw", "true", "try", "typealias", "typeof", "val", - "var", "when", "while", "by", "catch", "constructor", "delegate", - "dynamic", "field", "file", "finally", "get", "import", "init", - "param", "property", "receiver", "set", "setparam", "where", - "actual", "abstract", "annotation", "companion", "const", "crossinline", - "data", "enum", "expect", "external", "final", "infix", "inline", - "inner", "internal", "lateinit", "noinline", "open", "operator", - "out", "override", "private", "protected", "public", "reified", - "sealed", "suspend", "tailrec", "vararg" - ) - } - - fun provideSemanticTokens(uri: String): SemanticTokens? { - val state = documentManager.get(uri) ?: return null - - analysisScheduler.analyzeSync(uri) - - val syntaxTree = state.syntaxTree ?: return null - val symbolTable = state.symbolTable - val analysisContext = state.analysisContext - - val tokens = mutableListOf() - collectTokens(syntaxTree.root, symbolTable, analysisContext, tokens) - - tokens.sortWith(compareBy({ it.line }, { it.column })) - - val data = encodeTokens(tokens) - return SemanticTokens(data) - } - - private fun collectTokens( - node: SyntaxNode, - symbolTable: SymbolTable?, - analysisContext: AnalysisContext?, - tokens: MutableList - ) { - val token = classifyNode(node, symbolTable, analysisContext) - if (token != null) { - tokens.add(token) - } - - for (child in node.children) { - collectTokens(child, symbolTable, analysisContext, tokens) - } - } - - private fun classifyNode(node: SyntaxNode, symbolTable: SymbolTable?, analysisContext: AnalysisContext?): SemanticToken? { - return when (node.kind) { - SyntaxKind.COMMENT, - SyntaxKind.MULTILINE_COMMENT -> { - createToken(node, TYPE_COMMENT, 0) - } - - SyntaxKind.STRING_LITERAL, - SyntaxKind.LINE_STRING_LITERAL, - SyntaxKind.MULTI_LINE_STRING_LITERAL, - SyntaxKind.CHARACTER_LITERAL -> { - createToken(node, TYPE_STRING, 0) - } - - SyntaxKind.INTEGER_LITERAL, - SyntaxKind.LONG_LITERAL, - SyntaxKind.HEX_LITERAL, - SyntaxKind.BIN_LITERAL, - SyntaxKind.REAL_LITERAL -> { - createToken(node, TYPE_NUMBER, 0) - } - - SyntaxKind.BOOLEAN_LITERAL -> { - createToken(node, TYPE_KEYWORD, 0) - } - - SyntaxKind.NULL_LITERAL -> { - createToken(node, TYPE_KEYWORD, 0) - } - - SyntaxKind.SIMPLE_IDENTIFIER -> { - classifyIdentifier(node, symbolTable, analysisContext) - } - - SyntaxKind.TYPE_IDENTIFIER -> { - createToken(node, TYPE_TYPE, 0) - } - - SyntaxKind.ANNOTATION -> { - createToken(node, TYPE_DECORATOR, 0) - } - - SyntaxKind.LABEL -> { - createToken(node, TYPE_KEYWORD, 0) - } - - else -> { - if (isKeywordNode(node)) { - createToken(node, TYPE_KEYWORD, 0) - } else if (isModifierNode(node)) { - createToken(node, TYPE_MODIFIER, 0) - } else { - null - } - } - } - } - - private fun classifyIdentifier(node: SyntaxNode, symbolTable: SymbolTable?, analysisContext: AnalysisContext?): SemanticToken? { - val text = node.text - if (text.isEmpty()) return null - - if (text in KOTLIN_KEYWORDS) { - return createToken(node, TYPE_KEYWORD, 0) - } - - val parent = node.parent - - if (parent != null) { - when (parent.kind) { - SyntaxKind.FUNCTION_DECLARATION -> { - val nameChild = parent.childByFieldName("name") - if (nameChild != null && isSameNode(nameChild, node)) { - return createToken(node, TYPE_FUNCTION, MOD_DECLARATION or MOD_DEFINITION) - } - } - - SyntaxKind.CLASS_DECLARATION -> { - val nameChild = parent.childByFieldName("name") - if (nameChild != null && isSameNode(nameChild, node)) { - val kind = determineClassKind(parent) - return createToken(node, kind, MOD_DECLARATION or MOD_DEFINITION) - } - } - - SyntaxKind.OBJECT_DECLARATION -> { - val nameChild = parent.childByFieldName("name") - if (nameChild != null && isSameNode(nameChild, node)) { - return createToken(node, TYPE_CLASS, MOD_DECLARATION or MOD_DEFINITION) - } - } - - SyntaxKind.PROPERTY_DECLARATION -> { - val nameChild = parent.childByFieldName("name") - if (nameChild != null && isSameNode(nameChild, node)) { - val isVal = parent.children.any { it.text == "val" } - val mods = MOD_DECLARATION or MOD_DEFINITION or (if (isVal) MOD_READONLY else 0) - return createToken(node, TYPE_PROPERTY, mods) - } - } - - SyntaxKind.PARAMETER -> { - val nameChild = parent.childByFieldName("name") - if (nameChild != null && isSameNode(nameChild, node)) { - return createToken(node, TYPE_PARAMETER, MOD_DECLARATION) - } - } - - SyntaxKind.TYPE_PARAMETER -> { - val nameChild = parent.childByFieldName("name") - if (nameChild != null && isSameNode(nameChild, node)) { - return createToken(node, TYPE_TYPE_PARAMETER, MOD_DECLARATION) - } - } - - SyntaxKind.ENUM_ENTRY -> { - return createToken(node, TYPE_ENUM_MEMBER, MOD_READONLY) - } - - SyntaxKind.IMPORT_HEADER -> { - return null - } - - SyntaxKind.PACKAGE_HEADER -> { - return createToken(node, TYPE_NAMESPACE, 0) - } - - SyntaxKind.CALL_EXPRESSION -> { - val functionNode = parent.childByFieldName("function") - ?: parent.children.firstOrNull { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - if (functionNode != null && isSameNode(functionNode, node)) { - return createToken(node, TYPE_FUNCTION, 0) - } - } - - SyntaxKind.USER_TYPE, - SyntaxKind.SIMPLE_USER_TYPE -> { - return createToken(node, TYPE_TYPE, 0) - } - - SyntaxKind.NAVIGATION_EXPRESSION -> { - val suffix = parent.childByFieldName("suffix") - ?: parent.children.lastOrNull { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - if (suffix != null && isSameNode(suffix, node)) { - val resolvedSymbol = analysisContext?.getResolvedSymbol(node) - if (resolvedSymbol != null) { - return classifyFromSymbol(node, resolvedSymbol) - } - val grandparent = parent.parent - if (grandparent?.kind == SyntaxKind.CALL_EXPRESSION) { - return createToken(node, TYPE_METHOD, 0) - } - return createToken(node, TYPE_PROPERTY, 0) - } - } - - else -> {} - } - } - - val resolvedSymbol = analysisContext?.getResolvedSymbol(node) - if (resolvedSymbol != null) { - return classifyFromSymbol(node, resolvedSymbol) - } - - if (symbolTable != null) { - val position = Position(node.startLine, node.startColumn) - val resolved = symbolTable.resolve(text, position) - val symbol = resolved.firstOrNull() - - if (symbol != null) { - return classifyFromSymbol(node, symbol) - } - } - - if (text.first().isUpperCase()) { - return createToken(node, TYPE_TYPE, 0) - } - - return createToken(node, TYPE_VARIABLE, 0) - } - - private fun classifyFromSymbol(node: SyntaxNode, symbol: Symbol): SemanticToken { - return when (symbol) { - is ClassSymbol -> { - val kind = when { - symbol.isInterface -> TYPE_INTERFACE - symbol.isEnum -> TYPE_ENUM - symbol.isObject -> TYPE_CLASS - symbol.isAnnotation -> TYPE_DECORATOR - else -> TYPE_CLASS - } - val mods = if (symbol.modifiers.isAbstract) MOD_ABSTRACT else 0 - createToken(node, kind, mods)!! - } - is FunctionSymbol -> { - val mods = buildFunctionModifiers(symbol) - createToken(node, TYPE_FUNCTION, mods)!! - } - is PropertySymbol -> { - val mods = if (!symbol.isVar) MOD_READONLY else 0 - createToken(node, TYPE_PROPERTY, mods)!! - } - is ParameterSymbol -> createToken(node, TYPE_PARAMETER, 0)!! - is TypeParameterSymbol -> createToken(node, TYPE_TYPE_PARAMETER, 0)!! - is TypeAliasSymbol -> createToken(node, TYPE_TYPE, 0)!! - else -> createToken(node, TYPE_VARIABLE, 0)!! - } - } - - private fun buildFunctionModifiers(symbol: FunctionSymbol): Int { - var mods = 0 - if (symbol.modifiers.isAbstract) mods = mods or MOD_ABSTRACT - if (symbol.isSuspend) mods = mods or (1 shl 6) - return mods - } - - private fun determineClassKind(classNode: SyntaxNode): Int { - for (child in classNode.children) { - val text = child.text - when { - text == "interface" -> return TYPE_INTERFACE - text == "enum" -> return TYPE_ENUM - text == "annotation" -> return TYPE_DECORATOR - text == "object" -> return TYPE_CLASS - } - } - return TYPE_CLASS - } - - private fun isKeywordNode(node: SyntaxNode): Boolean { - val text = node.text - return text in KOTLIN_KEYWORDS || - node.kind == SyntaxKind.THIS || - node.kind == SyntaxKind.SUPER || - node.kind == SyntaxKind.RETURN || - node.kind == SyntaxKind.THROW || - node.kind == SyntaxKind.IF || - node.kind == SyntaxKind.ELSE || - node.kind == SyntaxKind.WHEN || - node.kind == SyntaxKind.WHILE || - node.kind == SyntaxKind.FOR || - node.kind == SyntaxKind.DO || - node.kind == SyntaxKind.TRY || - node.kind == SyntaxKind.CATCH || - node.kind == SyntaxKind.FINALLY || - node.kind == SyntaxKind.BREAK || - node.kind == SyntaxKind.CONTINUE - } - - private fun isModifierNode(node: SyntaxNode): Boolean { - val parent = node.parent ?: return false - return parent.kind == SyntaxKind.MODIFIERS || - parent.kind == SyntaxKind.VISIBILITY_MODIFIER || - parent.kind == SyntaxKind.INHERITANCE_MODIFIER || - parent.kind == SyntaxKind.MEMBER_MODIFIER || - parent.kind == SyntaxKind.FUNCTION_MODIFIER || - parent.kind == SyntaxKind.PROPERTY_MODIFIER || - parent.kind == SyntaxKind.PARAMETER_MODIFIER || - parent.kind == SyntaxKind.CLASS_MODIFIER - } - - private fun isSameNode(a: SyntaxNode, b: SyntaxNode): Boolean { - return a.startLine == b.startLine && a.startColumn == b.startColumn - } - - private fun createToken(node: SyntaxNode, tokenType: Int, modifiers: Int): SemanticToken? { - val text = node.text - if (text.isEmpty()) return null - - val lines = text.split('\n') - if (lines.size == 1) { - return SemanticToken( - line = node.startLine, - column = node.startColumn, - length = text.length, - tokenType = tokenType, - modifiers = modifiers - ) - } - - return SemanticToken( - line = node.startLine, - column = node.startColumn, - length = lines.first().length, - tokenType = tokenType, - modifiers = modifiers - ) - } - - private fun encodeTokens(tokens: List): List { - val data = mutableListOf() - var prevLine = 0 - var prevColumn = 0 - - for (token in tokens) { - val deltaLine = token.line - prevLine - val deltaColumn = if (deltaLine == 0) token.column - prevColumn else token.column - - data.add(deltaLine) - data.add(deltaColumn) - data.add(token.length) - data.add(token.tokenType) - data.add(token.modifiers) - - prevLine = token.line - prevColumn = token.column - } - - return data - } - - private data class SemanticToken( - val line: Int, - val column: Int, - val length: Int, - val tokenType: Int, - val modifiers: Int - ) -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Modifiers.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Modifiers.kt deleted file mode 100644 index 3439ae7446..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Modifiers.kt +++ /dev/null @@ -1,247 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -/** - * Modality of a class or member declaration. - * - * Modality controls inheritance and override behavior: - * - * - [FINAL]: Cannot be overridden or subclassed (default for classes and members) - * - [OPEN]: Can be overridden or subclassed - * - [ABSTRACT]: Must be overridden, has no implementation - * - [SEALED]: Can only be subclassed within the same module - */ -enum class Modality { - FINAL, - OPEN, - ABSTRACT, - SEALED; - - companion object { - val DEFAULT: Modality = FINAL - - fun fromKeyword(keyword: String): Modality? = when (keyword) { - "final" -> FINAL - "open" -> OPEN - "abstract" -> ABSTRACT - "sealed" -> SEALED - else -> null - } - } -} - -/** - * Kind of class-like declaration. - */ -enum class ClassKind { - CLASS, - INTERFACE, - OBJECT, - ENUM_CLASS, - ENUM_ENTRY, - ANNOTATION_CLASS, - DATA_CLASS, - VALUE_CLASS, - COMPANION_OBJECT; - - val isObject: Boolean get() = this == OBJECT || this == COMPANION_OBJECT || this == ENUM_ENTRY - val isClass: Boolean get() = this != INTERFACE - val isInterface: Boolean get() = this == INTERFACE -} - -/** - * Collection of modifiers that can be applied to Kotlin declarations. - * - * This immutable data class captures all modifier information for a declaration: - * - Visibility ([visibility]) - * - Modality ([modality]) - * - Function modifiers (inline, suspend, operator, etc.) - * - Property modifiers (const, lateinit) - * - Class modifiers (data, value, inner, etc.) - * - Parameter modifiers (vararg, crossinline, noinline) - * - Platform modifiers (expect, actual) - * - * ## Usage - * - * ```kotlin - * val modifiers = Modifiers( - * visibility = Visibility.PUBLIC, - * modality = Modality.OPEN, - * isSuspend = true - * ) - * - * if (modifiers.isSuspend) { - * // Handle suspend function - * } - * ``` - * - * @see Visibility - * @see Modality - */ -data class Modifiers( - val visibility: Visibility = Visibility.DEFAULT, - val modality: Modality = Modality.DEFAULT, - - val isInline: Boolean = false, - val isSuspend: Boolean = false, - val isTailrec: Boolean = false, - val isOperator: Boolean = false, - val isInfix: Boolean = false, - val isExternal: Boolean = false, - - val isConst: Boolean = false, - val isLateInit: Boolean = false, - - val isData: Boolean = false, - val isValue: Boolean = false, - val isInner: Boolean = false, - val isCompanion: Boolean = false, - val isAnnotation: Boolean = false, - val isEnum: Boolean = false, - val isFunInterface: Boolean = false, - - val isVararg: Boolean = false, - val isCrossinline: Boolean = false, - val isNoinline: Boolean = false, - val isReified: Boolean = false, - - val isOverride: Boolean = false, - - val isExpect: Boolean = false, - val isActual: Boolean = false -) { - /** - * Whether this declaration has explicit visibility. - */ - val hasExplicitVisibility: Boolean - get() = visibility != Visibility.DEFAULT - - /** - * Whether this declaration is abstract (no implementation). - */ - val isAbstract: Boolean - get() = modality == Modality.ABSTRACT - - /** - * Whether this declaration is open for override/subclassing. - */ - val isOpen: Boolean - get() = modality == Modality.OPEN || modality == Modality.ABSTRACT - - /** - * Whether this declaration is final (cannot be overridden). - */ - val isFinal: Boolean - get() = modality == Modality.FINAL - - /** - * Whether this declaration is sealed. - */ - val isSealed: Boolean - get() = modality == Modality.SEALED - - /** - * Whether this is a private declaration. - */ - val isPrivate: Boolean - get() = visibility == Visibility.PRIVATE - - /** - * Whether this is a protected declaration. - */ - val isProtected: Boolean - get() = visibility == Visibility.PROTECTED - - /** - * Whether this is an internal declaration. - */ - val isInternal: Boolean - get() = visibility == Visibility.INTERNAL - - /** - * Whether this is a public declaration. - */ - val isPublic: Boolean - get() = visibility == Visibility.PUBLIC - - /** - * Determines the [ClassKind] based on these modifiers. - * - * @param isInterface Whether the declaration is an interface - * @param isObject Whether the declaration is an object - */ - fun toClassKind(isInterface: Boolean = false, isObject: Boolean = false): ClassKind { - return when { - isInterface && isFunInterface -> ClassKind.INTERFACE - isInterface -> ClassKind.INTERFACE - isCompanion -> ClassKind.COMPANION_OBJECT - isObject -> ClassKind.OBJECT - isEnum -> ClassKind.ENUM_CLASS - isAnnotation -> ClassKind.ANNOTATION_CLASS - isData -> ClassKind.DATA_CLASS - isValue -> ClassKind.VALUE_CLASS - else -> ClassKind.CLASS - } - } - - /** - * Creates a copy with updated visibility. - */ - fun withVisibility(visibility: Visibility): Modifiers = copy(visibility = visibility) - - /** - * Creates a copy with updated modality. - */ - fun withModality(modality: Modality): Modifiers = copy(modality = modality) - - /** - * Merges with another [Modifiers] instance, preferring non-default values from [other]. - */ - fun merge(other: Modifiers): Modifiers = Modifiers( - visibility = if (other.visibility != Visibility.DEFAULT) other.visibility else visibility, - modality = if (other.modality != Modality.DEFAULT) other.modality else modality, - isInline = isInline || other.isInline, - isSuspend = isSuspend || other.isSuspend, - isTailrec = isTailrec || other.isTailrec, - isOperator = isOperator || other.isOperator, - isInfix = isInfix || other.isInfix, - isExternal = isExternal || other.isExternal, - isConst = isConst || other.isConst, - isLateInit = isLateInit || other.isLateInit, - isData = isData || other.isData, - isValue = isValue || other.isValue, - isInner = isInner || other.isInner, - isCompanion = isCompanion || other.isCompanion, - isAnnotation = isAnnotation || other.isAnnotation, - isEnum = isEnum || other.isEnum, - isFunInterface = isFunInterface || other.isFunInterface, - isVararg = isVararg || other.isVararg, - isCrossinline = isCrossinline || other.isCrossinline, - isNoinline = isNoinline || other.isNoinline, - isReified = isReified || other.isReified, - isOverride = isOverride || other.isOverride, - isExpect = isExpect || other.isExpect, - isActual = isActual || other.isActual - ) - - companion object { - /** - * Default modifiers (public, final, no special modifiers). - */ - val EMPTY: Modifiers = Modifiers() - - /** - * Modifiers for an interface (public, abstract). - */ - val INTERFACE: Modifiers = Modifiers(modality = Modality.ABSTRACT) - - /** - * Modifiers for abstract members. - */ - val ABSTRACT: Modifiers = Modifiers(modality = Modality.ABSTRACT) - - /** - * Modifiers for open members. - */ - val OPEN: Modifiers = Modifiers(modality = Modality.OPEN) - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Scope.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Scope.kt deleted file mode 100644 index 9261afaf69..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Scope.kt +++ /dev/null @@ -1,298 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -import org.appdevforall.codeonthego.lsp.kotlin.parser.TextRange - -/** - * A lexical scope containing symbol definitions. - * - * Scopes form a tree structure mirroring the syntactic nesting of Kotlin code. - * Each scope contains symbols defined at that level and has a reference to its - * parent scope for resolution. - * - * ## Resolution Algorithm - * - * Symbol resolution proceeds from the innermost scope outward: - * 1. Search the current scope - * 2. If not found, search the parent scope - * 3. Continue until found or reaching the root scope - * - * ## Overloading - * - * Functions can be overloaded (multiple functions with the same name). - * When resolving a function name, all overloads in the same scope are returned. - * - * ## Example - * - * ```kotlin - * // File scope - * class Foo { // Class scope (parent = file) - * fun bar() { // Function scope (parent = class) - * val x = 1 // Block scope (parent = function) - * } - * } - * ``` - * - * @property kind The kind of scope (file, class, function, etc.) - * @property parent The enclosing scope, or null for the root scope - * @property owner The symbol that owns this scope (e.g., class symbol for class scope) - * @property range The source range covered by this scope - */ -class Scope( - val kind: ScopeKind, - val parent: Scope? = null, - owner: Symbol? = null, - val range: TextRange = TextRange.EMPTY -) { - var owner: Symbol? = owner - internal set - private val symbols: MutableMap> = mutableMapOf() - private val childScopes: MutableList = mutableListOf() - - /** - * The depth of this scope in the scope tree. - * Root scope has depth 0. - */ - val depth: Int by lazy { - var d = 0 - var current = parent - while (current != null) { - d++ - current = current.parent - } - d - } - - /** - * All symbols defined directly in this scope. - */ - val allSymbols: List - get() = symbols.values.flatten() - - /** - * All symbol names defined in this scope. - */ - val symbolNames: Set - get() = symbols.keys.toSet() - - /** - * Number of symbols in this scope. - */ - val symbolCount: Int - get() = symbols.values.sumOf { it.size } - - /** - * Whether this scope is empty. - */ - val isEmpty: Boolean - get() = symbols.isEmpty() - - /** - * Child scopes nested within this scope. - */ - val children: List - get() = childScopes.toList() - - /** - * Defines a symbol in this scope. - * - * Symbols with the same name are added to the overload list only if - * they can be overloaded (both must be functions). Non-function symbols - * with duplicate names are silently ignored. - * - * @param symbol The symbol to define - * @return true if the symbol was added, false if it was a non-overloadable duplicate - */ - fun define(symbol: Symbol): Boolean { - val existing = symbols[symbol.name] - if (existing != null) { - if (!canOverload(existing.first(), symbol)) { - return false - } - existing.add(symbol) - } else { - symbols[symbol.name] = mutableListOf(symbol) - } - return true - } - - /** - * Removes a symbol from this scope. - * - * @param symbol The symbol to remove - * @return true if the symbol was found and removed - */ - fun undefine(symbol: Symbol): Boolean { - val list = symbols[symbol.name] ?: return false - val removed = list.remove(symbol) - if (list.isEmpty()) { - symbols.remove(symbol.name) - } - return removed - } - - /** - * Resolves a name in this scope only, without checking parent scopes. - * - * @param name The symbol name to find - * @return List of symbols with that name, or empty if not found - */ - fun resolveLocal(name: String): List { - return symbols[name]?.toList() ?: emptyList() - } - - /** - * Resolves a name by searching this scope and all parent scopes. - * - * @param name The symbol name to find - * @return List of symbols with that name from the first scope containing it - */ - fun resolve(name: String): List { - val local = resolveLocal(name) - if (local.isNotEmpty()) return local - - if (kind.isClassScope) { - (owner as? ClassSymbol)?.memberScope?.resolveLocal(name) - ?.takeIf { it.isNotEmpty() } - ?.let { return it } - } - - return parent?.resolve(name) ?: emptyList() - } - - /** - * Resolves a name and returns the first match. - * - * @param name The symbol name to find - * @return The first symbol with that name, or null if not found - */ - fun resolveFirst(name: String): Symbol? = resolve(name).firstOrNull() - - /** - * Collects all symbols matching a predicate from this scope and parents. - * - * @param predicate The condition symbols must match - * @return All matching symbols in resolution order - */ - fun collectAll(predicate: (Symbol) -> Boolean): List { - val result = mutableListOf() - var current: Scope? = this - while (current != null) { - result.addAll(current.allSymbols.filter(predicate)) - if (current.kind.isClassScope) { - (current.owner as? ClassSymbol)?.memberScope?.allSymbols - ?.filter(predicate) - ?.let { result.addAll(it) } - } - current = current.parent - } - return result - } - - /** - * Finds all symbols of a specific type in this scope. - */ - inline fun findAll(): List { - return allSymbols.filterIsInstance() - } - - /** - * Finds all symbols of a specific type by name. - */ - inline fun find(name: String): List { - return resolve(name).filterIsInstance() - } - - /** - * Creates a child scope. - * - * @param kind The kind of child scope - * @param owner The symbol owning the child scope - * @param range The source range of the child scope - * @return The new child scope - */ - fun createChild( - kind: ScopeKind, - owner: Symbol? = null, - range: TextRange = TextRange.EMPTY - ): Scope { - val child = Scope(kind, parent = this, owner = owner, range = range) - childScopes.add(child) - return child - } - - /** - * Gets the sequence of ancestor scopes from this scope to the root. - */ - fun ancestors(): Sequence = sequence { - var current = parent - while (current != null) { - yield(current) - current = current.parent - } - } - - /** - * Gets the root (file) scope. - */ - fun root(): Scope { - var current = this - while (current.parent != null) { - current = current.parent!! - } - return current - } - - /** - * Finds the nearest enclosing scope of a specific kind. - */ - fun findEnclosing(kind: ScopeKind): Scope? { - if (this.kind == kind) return this - return parent?.findEnclosing(kind) - } - - /** - * Finds the nearest enclosing class scope. - */ - fun findEnclosingClass(): Scope? { - return ancestors().find { it.kind.isClassScope } - } - - /** - * Finds the nearest enclosing callable scope (function, lambda, constructor). - * Checks the current scope first, then ancestors. - */ - fun findEnclosingCallable(): Scope? { - if (this.kind.isCallableScope) return this - return ancestors().find { it.kind.isCallableScope } - } - - /** - * Checks if this scope is nested within another scope. - */ - fun isNestedIn(other: Scope): Boolean { - return ancestors().any { it === other } - } - - /** - * Gets the enclosing class symbol, if any. - */ - fun getEnclosingClass(): ClassSymbol? { - return findEnclosingClass()?.owner as? ClassSymbol - } - - /** - * Gets the enclosing function symbol, if any. - */ - fun getEnclosingFunction(): FunctionSymbol? { - return findEnclosingCallable()?.owner as? FunctionSymbol - } - - private fun canOverload(existing: Symbol, new: Symbol): Boolean { - return existing is FunctionSymbol && new is FunctionSymbol - } - - override fun toString(): String { - val ownerName = owner?.name ?: "" - return "Scope($kind, owner=$ownerName, symbols=$symbolCount)" - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/ScopeKind.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/ScopeKind.kt deleted file mode 100644 index af04a386fe..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/ScopeKind.kt +++ /dev/null @@ -1,199 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -/** - * Kind of lexical scope in Kotlin. - * - * Scopes determine visibility and shadowing rules for symbol resolution. - * Each scope kind has specific behavior for: - * - What symbols can be defined - * - How symbols are resolved - * - Whether symbols can escape the scope - * - * ## Scope Hierarchy - * - * Scopes form a parent chain from innermost to outermost: - * ``` - * FILE -> CLASS -> FUNCTION -> BLOCK -> LAMBDA - * ``` - * - * Resolution proceeds from innermost scope outward until a match is found. - * - * ## Shadowing Rules - * - * - Inner scopes can shadow outer scope symbols - * - Overloading is allowed (multiple functions with same name) - * - Properties and functions can share the same name - */ -enum class ScopeKind { - /** - * Package/file scope. - * - * Contains: - * - Top-level declarations (classes, functions, properties) - * - Imported symbols - * - * This is the outermost scope for a file. - */ - FILE, - - /** - * Package scope (for cross-file resolution). - * - * Contains all top-level declarations in a package across all files. - */ - PACKAGE, - - /** - * Class/interface/object body scope. - * - * Contains: - * - Member functions - * - Member properties - * - Nested classes - * - Companion object (as a single symbol) - * - * Has implicit `this` reference to the containing class instance. - */ - CLASS, - - /** - * Enum class body scope. - * - * Contains: - * - Enum entries - * - Member functions and properties - */ - ENUM, - - /** - * Companion object scope. - * - * Contains static-like members that can be accessed - * without an instance of the containing class. - */ - COMPANION, - - /** - * Function/method body scope. - * - * Contains: - * - Parameters - * - Local variables - * - Local functions - * - * Parameters are visible throughout the function body. - */ - FUNCTION, - - /** - * Constructor scope. - * - * Contains: - * - Primary constructor parameters (also visible as properties) - * - Secondary constructor parameters - */ - CONSTRUCTOR, - - /** - * Property accessor scope (getter/setter). - * - * Contains: - * - Implicit `value` parameter (setter only) - * - Implicit `field` reference - */ - ACCESSOR, - - /** - * Lambda expression scope. - * - * Contains: - * - Lambda parameters - * - Implicit `it` parameter (single-param lambdas) - * - * Captures symbols from enclosing scopes. - */ - LAMBDA, - - /** - * Block scope (if, when, for, while, try bodies). - * - * Contains: - * - Variables declared with val/var - * - For loop variables - * - When subject (`when (val x = ...)`) - * - * Block-scoped symbols are not visible outside. - */ - BLOCK, - - /** - * Catch block scope. - * - * Contains: - * - Exception parameter - */ - CATCH, - - /** - * For loop scope. - * - * Contains: - * - Loop variable - */ - FOR_LOOP, - - /** - * When entry scope. - * - * Contains: - * - Smart cast information - * - Destructured variables - */ - WHEN_ENTRY, - - /** - * Type parameter scope. - * - * Contains: - * - Type parameters of a generic declaration - */ - TYPE_PARAMETER; - - /** - * Whether this scope can contain type declarations. - */ - val canContainTypes: Boolean - get() = this == FILE || this == PACKAGE || this == CLASS || this == COMPANION - - /** - * Whether this scope can contain functions. - */ - val canContainFunctions: Boolean - get() = this != TYPE_PARAMETER && this != CATCH && this != FOR_LOOP - - /** - * Whether this scope can contain properties. - */ - val canContainProperties: Boolean - get() = this == FILE || this == CLASS || this == COMPANION - - /** - * Whether this scope can contain local variables. - */ - val canContainLocals: Boolean - get() = this == FUNCTION || this == LAMBDA || this == BLOCK || - this == CONSTRUCTOR || this == ACCESSOR || this == CATCH || - this == FOR_LOOP || this == WHEN_ENTRY - - /** - * Whether this is a class-like scope (class, interface, object, enum). - */ - val isClassScope: Boolean - get() = this == CLASS || this == ENUM || this == COMPANION - - /** - * Whether this is a callable scope (function, lambda, constructor). - */ - val isCallableScope: Boolean - get() = this == FUNCTION || this == LAMBDA || this == CONSTRUCTOR || this == ACCESSOR -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Symbol.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Symbol.kt deleted file mode 100644 index b0325789f8..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Symbol.kt +++ /dev/null @@ -1,657 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -import org.appdevforall.codeonthego.lsp.kotlin.parser.TextRange - -/** - * Base class for all Kotlin symbols. - * - * A symbol represents a named entity in Kotlin source code. Symbols form - * a hierarchy that mirrors Kotlin's declaration kinds: - * - * - [ClassSymbol]: Classes, interfaces, objects, enums - * - [FunctionSymbol]: Functions and methods - * - [PropertySymbol]: Properties and variables - * - [ParameterSymbol]: Function and constructor parameters - * - [TypeParameterSymbol]: Generic type parameters - * - [TypeAliasSymbol]: Type aliases - * - [PackageSymbol]: Package declarations - * - * ## Symbol Properties - * - * All symbols have: - * - [name]: The declared name - * - [location]: Where the symbol is declared - * - [modifiers]: Access modifiers and other flags - * - [containingScope]: The scope where the symbol is defined - * - * ## Example - * - * ```kotlin - * class Foo { - * fun bar(x: Int): String = x.toString() - * } - * ``` - * - * This creates: - * - ClassSymbol("Foo") - * - FunctionSymbol("bar") - * - ParameterSymbol("x") - * - * @see Scope - * @see SymbolLocation - * @see Modifiers - */ -sealed class Symbol { - /** - * The name of this symbol as declared in source code. - */ - abstract val name: String - - /** - * The source location where this symbol is declared. - */ - abstract val location: SymbolLocation - - /** - * The modifiers applied to this symbol. - */ - abstract val modifiers: Modifiers - - /** - * The scope containing this symbol. - */ - abstract val containingScope: Scope? - - /** - * A unique identifier for this symbol within its context. - */ - open val id: String get() = name - - /** - * The fully qualified name of this symbol. - */ - open val qualifiedName: String - get() = buildString { - containingScope?.owner?.qualifiedName?.let { - append(it) - append('.') - } - append(name) - } - - /** - * The visibility of this symbol. - */ - val visibility: Visibility get() = modifiers.visibility - - /** - * Whether this symbol is public. - */ - val isPublic: Boolean get() = modifiers.isPublic - - /** - * Whether this symbol is private. - */ - val isPrivate: Boolean get() = modifiers.isPrivate - - /** - * Whether this symbol is protected. - */ - val isProtected: Boolean get() = modifiers.isProtected - - /** - * Whether this symbol is internal. - */ - val isInternal: Boolean get() = modifiers.isInternal - - /** - * Whether this symbol is synthetic (no source location). - */ - val isSynthetic: Boolean get() = location.isSynthetic - - /** - * Accepts a symbol visitor. - */ - abstract fun accept(visitor: SymbolVisitor, data: D): R -} - -/** - * Represents a class, interface, object, or enum declaration. - * - * ## Class Kinds - * - * - Regular class: `class Foo` - * - Interface: `interface Foo` - * - Object: `object Foo` - * - Companion object: `companion object` - * - Enum class: `enum class Foo` - * - Annotation class: `annotation class Foo` - * - Data class: `data class Foo` - * - Value class: `value class Foo` - * - * @property kind The specific kind of class-like declaration - * @property typeParameters Generic type parameters - * @property superTypes Direct supertypes (extended class, implemented interfaces) - * @property memberScope The scope containing this class's members - */ -data class ClassSymbol( - override val name: String, - override val location: SymbolLocation, - override val modifiers: Modifiers, - override val containingScope: Scope?, - val kind: ClassKind, - val typeParameters: List = emptyList(), - val superTypes: List = emptyList(), - val memberScope: Scope? = null, - val primaryConstructor: FunctionSymbol? = null, - val companionObject: ClassSymbol? = null -) : Symbol() { - /** - * Whether this is an interface. - */ - val isInterface: Boolean get() = kind == ClassKind.INTERFACE - - /** - * Whether this is an object (including companion objects). - */ - val isObject: Boolean get() = kind.isObject - - /** - * Whether this is a companion object. - */ - val isCompanion: Boolean get() = kind == ClassKind.COMPANION_OBJECT - - /** - * Whether this is an enum class. - */ - val isEnum: Boolean get() = kind == ClassKind.ENUM_CLASS - - /** - * Whether this is a data class. - */ - val isData: Boolean get() = kind == ClassKind.DATA_CLASS - - /** - * Whether this is a value class (inline class). - */ - val isValue: Boolean get() = kind == ClassKind.VALUE_CLASS - - /** - * Whether this is an annotation class. - */ - val isAnnotation: Boolean get() = kind == ClassKind.ANNOTATION_CLASS - - /** - * All member symbols. - */ - val members: List get() = memberScope?.allSymbols ?: emptyList() - - /** - * Member functions (including inherited). - */ - val functions: List get() = memberScope?.findAll() ?: emptyList() - - /** - * Member properties. - */ - val properties: List get() = memberScope?.findAll() ?: emptyList() - - /** - * Nested classes. - */ - val nestedClasses: List get() = memberScope?.findAll() ?: emptyList() - - /** - * Secondary constructors. - */ - val secondaryConstructors: List - get() = functions.filter { it.isConstructor && it != primaryConstructor } - - /** - * All constructors (primary + secondary). - */ - val constructors: List - get() = listOfNotNull(primaryConstructor) + secondaryConstructors - - /** - * Finds a member by name. - */ - fun findMember(name: String): List = memberScope?.resolveLocal(name) ?: emptyList() - - override fun accept(visitor: SymbolVisitor, data: D): R = visitor.visitClass(this, data) - - override fun toString(): String = "${kind.name.lowercase()} $qualifiedName" -} - -/** - * Represents a function or method declaration. - * - * This includes: - * - Top-level functions - * - Member methods - * - Extension functions - * - Constructors - * - Property accessors - * - * @property parameters Function parameters - * @property typeParameters Generic type parameters - * @property returnType The declared or inferred return type - * @property receiverType Receiver type for extension functions - * @property bodyScope The scope containing the function body - */ -data class FunctionSymbol( - override val name: String, - override val location: SymbolLocation, - override val modifiers: Modifiers, - override val containingScope: Scope?, - val parameters: List = emptyList(), - val typeParameters: List = emptyList(), - val returnType: TypeReference? = null, - val receiverType: TypeReference? = null, - val bodyScope: Scope? = null, - val isConstructor: Boolean = false, - val isPrimaryConstructor: Boolean = false -) : Symbol() { - /** - * Whether this is an extension function. - */ - val isExtension: Boolean get() = receiverType != null - - /** - * Whether this is a suspend function. - */ - val isSuspend: Boolean get() = modifiers.isSuspend - - /** - * Whether this is an inline function. - */ - val isInline: Boolean get() = modifiers.isInline - - /** - * Whether this is an operator function. - */ - val isOperator: Boolean get() = modifiers.isOperator - - /** - * Whether this is an infix function. - */ - val isInfix: Boolean get() = modifiers.isInfix - - /** - * Whether this is a tailrec function. - */ - val isTailrec: Boolean get() = modifiers.isTailrec - - /** - * Whether this function overrides another. - */ - val isOverride: Boolean get() = modifiers.isOverride - - /** - * Whether this function has a body (not abstract/external). - */ - val hasBody: Boolean get() = !modifiers.isAbstract && !modifiers.isExternal - - /** - * The number of parameters. - */ - val parameterCount: Int get() = parameters.size - - /** - * Required parameters (no default value). - */ - val requiredParameters: List - get() = parameters.filter { !it.hasDefaultValue } - - /** - * The number of required parameters. - */ - val requiredParameterCount: Int get() = requiredParameters.size - - /** - * Parameters with default values. - */ - val optionalParameters: List - get() = parameters.filter { it.hasDefaultValue } - - /** - * Finds a parameter by name. - */ - fun findParameter(name: String): ParameterSymbol? = parameters.find { it.name == name } - - /** - * Unique signature for overload resolution. - */ - override val id: String - get() = buildString { - append(name) - append('(') - parameters.joinTo(this) { it.type?.render() ?: "_" } - append(')') - } - - override fun accept(visitor: SymbolVisitor, data: D): R = visitor.visitFunction(this, data) - - override fun toString(): String = buildString { - if (isSuspend) append("suspend ") - append("fun ") - receiverType?.let { append(it.render()).append('.') } - append(name) - if (typeParameters.isNotEmpty()) { - append('<') - typeParameters.joinTo(this) { it.name } - append('>') - } - append('(') - parameters.joinTo(this) { "${it.name}: ${it.type?.render() ?: "?"}" } - append(')') - returnType?.let { append(": ").append(it.render()) } - } -} - -/** - * Represents a property or variable declaration. - * - * This includes: - * - Top-level properties - * - Member properties - * - Local variables (val/var) - * - Extension properties - * - * @property type The declared or inferred type - * @property receiverType Receiver type for extension properties - * @property getter Custom getter function - * @property setter Custom setter function - * @property isVar Whether this is a mutable property (var) - */ -data class PropertySymbol( - override val name: String, - override val location: SymbolLocation, - override val modifiers: Modifiers, - override val containingScope: Scope?, - val type: TypeReference? = null, - val receiverType: TypeReference? = null, - val getter: FunctionSymbol? = null, - val setter: FunctionSymbol? = null, - val isVar: Boolean = false, - val hasInitializer: Boolean = false, - val isDelegated: Boolean = false -) : Symbol() { - /** - * Whether this is a val (immutable). - */ - val isVal: Boolean get() = !isVar - - /** - * Whether this is an extension property. - */ - val isExtension: Boolean get() = receiverType != null - - /** - * Whether this is a compile-time constant. - */ - val isConst: Boolean get() = modifiers.isConst - - /** - * Whether this is a lateinit property. - */ - val isLateInit: Boolean get() = modifiers.isLateInit - - /** - * Whether this property overrides another. - */ - val isOverride: Boolean get() = modifiers.isOverride - - /** - * Whether this has a custom getter. - */ - val hasCustomGetter: Boolean get() = getter != null - - /** - * Whether this has a custom setter. - */ - val hasCustomSetter: Boolean get() = setter != null - - override fun accept(visitor: SymbolVisitor, data: D): R = visitor.visitProperty(this, data) - - override fun toString(): String = buildString { - append(if (isVar) "var " else "val ") - receiverType?.let { append(it.render()).append('.') } - append(name) - type?.let { append(": ").append(it.render()) } - } -} - -/** - * Represents a function or constructor parameter. - * - * @property type The parameter type - * @property hasDefaultValue Whether the parameter has a default value - * @property isVararg Whether this is a vararg parameter - */ -data class ParameterSymbol( - override val name: String, - override val location: SymbolLocation, - override val modifiers: Modifiers, - override val containingScope: Scope?, - val type: TypeReference? = null, - val hasDefaultValue: Boolean = false, - val isVararg: Boolean = false, - val isCrossinline: Boolean = false, - val isNoinline: Boolean = false -) : Symbol() { - override fun accept(visitor: SymbolVisitor, data: D): R = visitor.visitParameter(this, data) - - override fun toString(): String = buildString { - if (isVararg) append("vararg ") - append(name) - type?.let { append(": ").append(it.render()) } - } -} - -/** - * Represents a generic type parameter. - * - * @property bounds Upper bounds for this type parameter - * @property variance Variance modifier (in/out/invariant) - * @property isReified Whether this is a reified type parameter - */ -data class TypeParameterSymbol( - override val name: String, - override val location: SymbolLocation, - override val modifiers: Modifiers, - override val containingScope: Scope?, - val bounds: List = emptyList(), - val variance: Variance = Variance.INVARIANT, - val isReified: Boolean = false -) : Symbol() { - /** - * Whether this type parameter has explicit bounds. - */ - val hasBounds: Boolean get() = bounds.isNotEmpty() - - /** - * The effective upper bound (first bound or Any?). - */ - val effectiveBound: TypeReference? - get() = bounds.firstOrNull() - - override fun accept(visitor: SymbolVisitor, data: D): R = visitor.visitTypeParameter(this, data) - - override fun toString(): String = buildString { - when (variance) { - Variance.IN -> append("in ") - Variance.OUT -> append("out ") - Variance.INVARIANT -> {} - } - if (isReified) append("reified ") - append(name) - if (bounds.isNotEmpty()) { - append(" : ") - bounds.joinTo(this) { it.render() } - } - } -} - -/** - * Variance modifier for type parameters. - */ -enum class Variance { - INVARIANT, - IN, - OUT; - - companion object { - fun fromKeyword(keyword: String): Variance = when (keyword) { - "in" -> IN - "out" -> OUT - else -> INVARIANT - } - } -} - -/** - * Represents a type alias declaration. - * - * @property typeParameters Generic type parameters - * @property underlyingType The aliased type - */ -data class TypeAliasSymbol( - override val name: String, - override val location: SymbolLocation, - override val modifiers: Modifiers, - override val containingScope: Scope?, - val typeParameters: List = emptyList(), - val underlyingType: TypeReference? = null -) : Symbol() { - override fun accept(visitor: SymbolVisitor, data: D): R = visitor.visitTypeAlias(this, data) - - override fun toString(): String = buildString { - append("typealias ") - append(name) - if (typeParameters.isNotEmpty()) { - append('<') - typeParameters.joinTo(this) { it.name } - append('>') - } - underlyingType?.let { append(" = ").append(it.render()) } - } -} - -/** - * Represents a package declaration. - * - * @property packageName The full package name (e.g., "kotlin.collections") - */ -data class PackageSymbol( - override val name: String, - override val location: SymbolLocation, - override val modifiers: Modifiers = Modifiers.EMPTY, - override val containingScope: Scope? = null, - val packageName: String = name -) : Symbol() { - /** - * Package name segments. - */ - val segments: List get() = packageName.split('.') - - override val qualifiedName: String get() = packageName - - override fun accept(visitor: SymbolVisitor, data: D): R = visitor.visitPackage(this, data) - - override fun toString(): String = "package $packageName" -} - -/** - * A reference to a type (unresolved). - * - * Type references are created during parsing before full type resolution. - * They hold the syntactic representation of a type. - * - * @property name The type name (may be qualified) - * @property typeArguments Type arguments for generic types - * @property isNullable Whether this is a nullable type (T?) - * @property range Source range of the type reference - */ -data class FunctionTypeInfo( - val receiverType: TypeReference?, - val parameterTypes: List, - val returnType: TypeReference -) - -data class TypeReference( - val name: String, - val typeArguments: List = emptyList(), - val isNullable: Boolean = false, - val range: TextRange = TextRange.EMPTY, - val functionTypeInfo: FunctionTypeInfo? = null -) { - val simpleName: String get() = name.substringAfterLast('.') - - val isGeneric: Boolean get() = typeArguments.isNotEmpty() - - val isFunctionType: Boolean get() = functionTypeInfo != null - - fun nullable(): TypeReference = copy(isNullable = true) - - fun nonNullable(): TypeReference = copy(isNullable = false) - - fun render(): String = buildString { - if (functionTypeInfo != null) { - val wrapInParens = isNullable - if (wrapInParens) append('(') - if (functionTypeInfo.receiverType != null) { - append(functionTypeInfo.receiverType.render()) - append(".") - } - append("(") - functionTypeInfo.parameterTypes.joinTo(this) { it.render() } - append(") -> ") - append(functionTypeInfo.returnType.render()) - if (wrapInParens) append(')') - } else { - append(name) - if (typeArguments.isNotEmpty()) { - append('<') - typeArguments.joinTo(this) { it.render() } - append('>') - } - } - if (isNullable) append('?') - } - - override fun toString(): String = render() - - companion object { - val UNIT = TypeReference("Unit") - val NOTHING = TypeReference("Nothing") - val ANY = TypeReference("Any") - val ANY_NULLABLE = TypeReference("Any", isNullable = true) - val STRING = TypeReference("String") - val INT = TypeReference("Int") - val BOOLEAN = TypeReference("Boolean") - - fun simple(name: String, nullable: Boolean = false): TypeReference { - return TypeReference(name, isNullable = nullable) - } - - fun generic(name: String, vararg args: TypeReference): TypeReference { - return TypeReference(name, args.toList()) - } - - fun functionType( - receiverType: TypeReference?, - parameterTypes: List, - returnType: TypeReference, - isNullable: Boolean = false, - range: TextRange = TextRange.EMPTY - ): TypeReference { - return TypeReference( - name = "Function", - isNullable = isNullable, - range = range, - functionTypeInfo = FunctionTypeInfo(receiverType, parameterTypes, returnType) - ) - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolBuilder.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolBuilder.kt deleted file mode 100644 index e9d656e25a..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolBuilder.kt +++ /dev/null @@ -1,1706 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -import android.util.Log -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxKind -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxNode -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxTree - -/** - * Builds symbol tables from parsed syntax trees. - * - * SymbolBuilder traverses a [SyntaxTree] and extracts all declarations, - * creating [Symbol] objects and organizing them into [Scope] hierarchies. - * - * ## Two-Pass Resolution - * - * Symbol building uses two passes: - * 1. **Declaration Pass**: Register all declared names in their scopes - * 2. **Resolution Pass**: Resolve type references and build relationships - * - * This two-pass approach handles forward references correctly. - * - * ## Usage - * - * ```kotlin - * val parser = KotlinParser() - * val result = parser.parse(source) - * val table = SymbolBuilder.build(result.tree, filePath) - * ``` - * - * @see SymbolTable - * @see Symbol - * @see Scope - */ -class SymbolBuilder private constructor( - private val tree: SyntaxTree, - private val filePath: String -) { - private val fileScope = Scope(ScopeKind.FILE, range = tree.root.range) - private var currentScope = fileScope - private var packageName = "" - - private val symbolTable: SymbolTable by lazy { - SymbolTable(filePath, packageName, fileScope, tree) - } - - /** - * Builds the symbol table from the syntax tree. - */ - private fun build(): SymbolTable { - val root = tree.root - - when (root.kind) { - SyntaxKind.SOURCE_FILE -> processSourceFile(root) - else -> { - for (child in root.namedChildren) { - when (child.kind) { - SyntaxKind.PACKAGE_HEADER -> processPackageHeader(child) - SyntaxKind.IMPORT_LIST -> processImportList(child) - SyntaxKind.CLASS_DECLARATION -> processClassDeclaration(child) - SyntaxKind.OBJECT_DECLARATION -> processObjectDeclaration(child) - SyntaxKind.INTERFACE_DECLARATION -> processInterfaceDeclaration(child) - SyntaxKind.FUNCTION_DECLARATION -> processFunctionDeclaration(child) - SyntaxKind.PROPERTY_DECLARATION -> processPropertyDeclaration(child) - SyntaxKind.TYPE_ALIAS -> processTypeAlias(child) - else -> {} - } - } - } - } - return symbolTable - } - - private fun processSourceFile(node: SyntaxNode) { - for (child in node.namedChildren) { - when (child.kind) { - SyntaxKind.PACKAGE_HEADER -> processPackageHeader(child) - SyntaxKind.IMPORT_LIST -> processImportList(child) - SyntaxKind.CLASS_DECLARATION -> processClassDeclaration(child) - SyntaxKind.OBJECT_DECLARATION -> processObjectDeclaration(child) - SyntaxKind.INTERFACE_DECLARATION -> processInterfaceDeclaration(child) - SyntaxKind.FUNCTION_DECLARATION -> processFunctionDeclaration(child) - SyntaxKind.PROPERTY_DECLARATION -> processPropertyDeclaration(child) - SyntaxKind.TYPE_ALIAS -> processTypeAlias(child) - SyntaxKind.ERROR -> { - processErrorNodeChildren(child) - } - else -> { - Log.w("SymbolBuilder", " unhandled kind: ${child.kind}") - } - } - } - } - - private fun processErrorNodeChildren(node: SyntaxNode) { - if (tryRecoverClassFromErrorNode(node)) { - return - } - - for (child in node.namedChildren) { - when (child.kind) { - SyntaxKind.CLASS_DECLARATION -> processClassDeclaration(child) - SyntaxKind.OBJECT_DECLARATION -> processObjectDeclaration(child) - SyntaxKind.INTERFACE_DECLARATION -> processInterfaceDeclaration(child) - SyntaxKind.FUNCTION_DECLARATION -> processFunctionDeclaration(child) - SyntaxKind.PROPERTY_DECLARATION -> processPropertyDeclaration(child) - SyntaxKind.TYPE_ALIAS -> processTypeAlias(child) - SyntaxKind.ERROR -> processErrorNodeChildren(child) - else -> { - if (child.namedChildren.isNotEmpty()) { - processErrorNodeChildren(child) - } - } - } - } - } - - private fun tryRecoverClassFromErrorNode(node: SyntaxNode): Boolean { - val text = node.text - val classMatch = CLASS_PATTERN.find(text) - val interfaceMatch = INTERFACE_PATTERN.find(text) - val objectMatch = OBJECT_PATTERN.find(text) - - - when { - classMatch != null -> { - val className = classMatch.groupValues[1] - val existingSymbol = currentScope.resolveFirst(className) - if (existingSymbol is ClassSymbol) { - val classScope = existingSymbol.memberScope - if (classScope != null) { - withScope(classScope, existingSymbol) { - tryRecoverClassBodyFromErrorNode(node) - } - return true - } - return false - } - var superTypes = extractSuperTypesFromErrorNode(node) - if (superTypes.isEmpty()) { - superTypes = extractSuperTypesFromText(text, classMatch.range.last + 1) - } - createRecoveredClassSymbol(node, className, ClassKind.CLASS, superTypes) - return true - } - interfaceMatch != null -> { - val interfaceName = interfaceMatch.groupValues[1] - val existingSymbol = currentScope.resolveFirst(interfaceName) - if (existingSymbol is ClassSymbol) { - val interfaceScope = existingSymbol.memberScope - if (interfaceScope != null) { - withScope(interfaceScope, existingSymbol) { - tryRecoverClassBodyFromErrorNode(node) - } - return true - } - return false - } - var superTypes = extractSuperTypesFromErrorNode(node) - if (superTypes.isEmpty()) { - superTypes = extractSuperTypesFromText(text, interfaceMatch.range.last + 1) - } - createRecoveredClassSymbol(node, interfaceName, ClassKind.INTERFACE, superTypes) - return true - } - objectMatch != null -> { - val objectName = objectMatch.groupValues[1] - val existingSymbol = currentScope.resolveFirst(objectName) - if (existingSymbol is ClassSymbol) { - val objectScope = existingSymbol.memberScope - if (objectScope != null) { - withScope(objectScope, existingSymbol) { - tryRecoverClassBodyFromErrorNode(node) - } - return true - } - return false - } - createRecoveredClassSymbol(node, objectName, ClassKind.OBJECT, emptyList()) - return true - } - } - return false - } - - private fun extractSuperTypesFromErrorNode(node: SyntaxNode): List { - val superTypes = mutableListOf() - - for (child in node.namedChildren) { - when (child.kind) { - SyntaxKind.DELEGATION_SPECIFIER, - SyntaxKind.ANNOTATED_DELEGATION_SPECIFIER, - SyntaxKind.CONSTRUCTOR_INVOCATION -> { - val type = extractTypeFromDelegationSpecifier(child) - if (type != null) { - superTypes.add(type) - } - } - SyntaxKind.USER_TYPE, SyntaxKind.SIMPLE_USER_TYPE -> { - val type = extractType(child) - if (type != null) { - superTypes.add(type) - } - } - SyntaxKind.DELEGATION_SPECIFIERS -> { - val nestedTypes = extractSuperTypes(child) - superTypes.addAll(nestedTypes) - } - else -> {} - } - } - - return superTypes - } - - private fun extractSuperTypesFromText(text: String, startIndex: Int): List { - val afterName = text.substring(startIndex.coerceAtMost(text.length)) - val colonIndex = afterName.indexOf(':') - if (colonIndex < 0) return emptyList() - - val afterColon = afterName.substring(colonIndex + 1) - val braceIndex = afterColon.indexOf('{') - val supertypesPart = if (braceIndex >= 0) afterColon.substring(0, braceIndex) else afterColon - - return SUPERTYPE_PATTERN.findAll(supertypesPart) - .mapNotNull { match -> - val typeName = match.groupValues[1].trim() - if (typeName.isNotEmpty() && !typeName.all { it == '.' }) TypeReference(typeName) else null - } - .toList() - } - - private fun createRecoveredClassSymbol(node: SyntaxNode, name: String, kind: ClassKind, superTypes: List) { - val classScope = currentScope.createChild( - kind = ScopeKind.CLASS, - range = node.range - ) - - val classSymbol = ClassSymbol( - name = name, - location = createLocation(node, node), - modifiers = Modifiers.EMPTY, - containingScope = currentScope, - kind = kind, - superTypes = superTypes, - memberScope = classScope - ) - - currentScope.define(classSymbol) - symbolTable.registerSymbol(classSymbol) - - withScope(classScope, classSymbol) { - tryRecoverClassBodyFromErrorNode(node) - } - } - private fun tryRecoverClassBodyFromErrorNode(node: SyntaxNode) { - for (child in node.namedChildren) { - when (child.kind) { - SyntaxKind.CLASS_BODY -> { - processClassBody(child) - } - SyntaxKind.FUNCTION_DECLARATION -> { - processFunctionDeclaration(child) - } - SyntaxKind.PROPERTY_DECLARATION -> { - processPropertyDeclaration(child) - - } - SyntaxKind.COMPANION_OBJECT -> processObjectDeclaration(child) - else -> { - if (child.namedChildren.isNotEmpty()) { - tryRecoverClassBodyFromErrorNode(child) - } - } - } - } - } - - private fun processPackageHeader(node: SyntaxNode) { - val identifier = node.childByFieldName("identifier") - ?: node.findChild(SyntaxKind.IDENTIFIER) - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - - packageName = extractQualifiedName(identifier) ?: "" - - if (packageName.isNotEmpty()) { - val packageSymbol = PackageSymbol( - name = packageName.substringAfterLast('.'), - location = createLocation(node, identifier), - packageName = packageName - ) - symbolTable.registerSymbol(packageSymbol) - } - } - - private fun processImportList(node: SyntaxNode) { - val imports = mutableListOf() - - for (child in node.namedChildren) { - if (child.kind == SyntaxKind.IMPORT_HEADER) { - val import = processImportHeader(child) - if (import != null) { - imports.add(import) - } - } - } - - symbolTable.imports = imports - } - - private fun processImportHeader(node: SyntaxNode): ImportInfo? { - val identifier = node.childByFieldName("identifier") - ?: node.findChild(SyntaxKind.IDENTIFIER) - ?: return null - - val fqName = extractQualifiedName(identifier) ?: return null - val isStar = node.children.any { it.text == "*" } - - val aliasNode = node.findChild(SyntaxKind.IMPORT_ALIAS) - val alias = aliasNode?.findChild(SyntaxKind.SIMPLE_IDENTIFIER)?.text - - return ImportInfo( - fqName = fqName, - alias = alias, - isStar = isStar, - range = node.range - ) - } - - private fun processClassDeclaration(node: SyntaxNode): ClassSymbol? { - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: node.findChild(SyntaxKind.TYPE_IDENTIFIER) - - if (nameNode == null) { - return null - } - - val name = nameNode.text - val modifiers = extractModifiers(node) - val kind = determineClassKind(node, modifiers) - val typeParameters = extractTypeParameters(node) - val superTypes = extractSuperTypes(node) - - val classScope = currentScope.createChild( - kind = ScopeKind.CLASS, - range = node.range - ) - - val primaryConstructor = extractPrimaryConstructor(node, classScope) - - val classSymbol = ClassSymbol( - name = name, - location = createLocation(node, nameNode), - modifiers = modifiers, - containingScope = currentScope, - kind = kind, - typeParameters = typeParameters, - superTypes = superTypes, - memberScope = classScope, - primaryConstructor = primaryConstructor - ) - - currentScope.define(classSymbol) - symbolTable.registerSymbol(classSymbol) - - val thisSymbol = PropertySymbol( - name = "this", - location = createLocation(node, nameNode), - modifiers = Modifiers.EMPTY, - containingScope = classScope, - type = TypeReference(name, typeParameters.map { TypeReference(it.name, emptyList()) }), - isVar = false - ) - classScope.define(thisSymbol) - - withScope(classScope, classSymbol) { - processClassBody(node.childByFieldName("body") ?: node.findChild(SyntaxKind.CLASS_BODY)) - } - - return classSymbol - } - - private fun processObjectDeclaration(node: SyntaxNode): ClassSymbol? { - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: return null - - val name = nameNode.text - val modifiers = extractModifiers(node) - val kind = if (node.kind == SyntaxKind.COMPANION_OBJECT) { - ClassKind.COMPANION_OBJECT - } else { - ClassKind.OBJECT - } - - val objectScope = currentScope.createChild( - kind = if (kind == ClassKind.COMPANION_OBJECT) ScopeKind.COMPANION else ScopeKind.CLASS, - range = node.range - ) - - val objectSymbol = ClassSymbol( - name = name, - location = createLocation(node, nameNode), - modifiers = modifiers.copy(isCompanion = kind == ClassKind.COMPANION_OBJECT), - containingScope = currentScope, - kind = kind, - memberScope = objectScope - ) - - currentScope.define(objectSymbol) - symbolTable.registerSymbol(objectSymbol) - - val thisSymbol = PropertySymbol( - name = "this", - location = createLocation(node, nameNode), - modifiers = Modifiers.EMPTY, - containingScope = objectScope, - type = TypeReference(name, emptyList()), - isVar = false - ) - objectScope.define(thisSymbol) - - withScope(objectScope, objectSymbol) { - processClassBody(node.childByFieldName("body") ?: node.findChild(SyntaxKind.CLASS_BODY)) - } - - return objectSymbol - } - - private fun processInterfaceDeclaration(node: SyntaxNode): ClassSymbol? { - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: node.findChild(SyntaxKind.TYPE_IDENTIFIER) - ?: return null - - val name = nameNode.text - val modifiers = extractModifiers(node).copy(modality = Modality.ABSTRACT) - val typeParameters = extractTypeParameters(node) - val superTypes = extractSuperTypes(node) - - val interfaceScope = currentScope.createChild( - kind = ScopeKind.CLASS, - range = node.range - ) - - val interfaceSymbol = ClassSymbol( - name = name, - location = createLocation(node, nameNode), - modifiers = modifiers, - containingScope = currentScope, - kind = ClassKind.INTERFACE, - typeParameters = typeParameters, - superTypes = superTypes, - memberScope = interfaceScope - ) - - currentScope.define(interfaceSymbol) - symbolTable.registerSymbol(interfaceSymbol) - - val thisSymbol = PropertySymbol( - name = "this", - location = createLocation(node, nameNode), - modifiers = Modifiers.EMPTY, - containingScope = interfaceScope, - type = TypeReference(name, typeParameters.map { TypeReference(it.name, emptyList()) }), - isVar = false - ) - interfaceScope.define(thisSymbol) - - withScope(interfaceScope, interfaceSymbol) { - processClassBody(node.childByFieldName("body") ?: node.findChild(SyntaxKind.CLASS_BODY)) - } - - return interfaceSymbol - } - - private fun processClassBody(node: SyntaxNode?) { - node ?: return - for (child in node.namedChildren) { - when (child.kind) { - SyntaxKind.FUNCTION_DECLARATION -> processFunctionDeclaration(child) - SyntaxKind.PROPERTY_DECLARATION -> processPropertyDeclaration(child) - SyntaxKind.CLASS_DECLARATION -> processClassDeclaration(child) - SyntaxKind.OBJECT_DECLARATION -> processObjectDeclaration(child) - SyntaxKind.COMPANION_OBJECT -> processObjectDeclaration(child) - SyntaxKind.INTERFACE_DECLARATION -> processInterfaceDeclaration(child) - SyntaxKind.SECONDARY_CONSTRUCTOR -> processSecondaryConstructor(child) - SyntaxKind.ANONYMOUS_INITIALIZER -> processInitBlock(child) - SyntaxKind.ENUM_CLASS_BODY -> processEnumClassBody(child) - else -> {} - } - } - } - - private fun processEnumClassBody(node: SyntaxNode) { - for (child in node.namedChildren) { - when (child.kind) { - SyntaxKind.ENUM_ENTRY -> processEnumEntry(child) - SyntaxKind.FUNCTION_DECLARATION -> processFunctionDeclaration(child) - SyntaxKind.PROPERTY_DECLARATION -> processPropertyDeclaration(child) - else -> {} - } - } - } - - private fun processEnumEntry(node: SyntaxNode): ClassSymbol? { - val nameNode = node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) ?: return null - val name = nameNode.text - - val entrySymbol = ClassSymbol( - name = name, - location = createLocation(node, nameNode), - modifiers = Modifiers.EMPTY, - containingScope = currentScope, - kind = ClassKind.ENUM_ENTRY - ) - - currentScope.define(entrySymbol) - symbolTable.registerSymbol(entrySymbol) - - return entrySymbol - } - - private fun processFunctionDeclaration(node: SyntaxNode): FunctionSymbol? { - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: return null - - val name = nameNode.text - val modifiers = extractModifiers(node) - val typeParameters = extractTypeParameters(node) - val receiverType = extractReceiverType(node) - val returnType = extractReturnType(node) - - val functionScope = currentScope.createChild( - kind = ScopeKind.FUNCTION, - range = node.range - ) - - val parameters = mutableListOf() - - withScope(functionScope, null) { - val paramsNode = node.childByFieldName("parameters") - ?: node.findChild(SyntaxKind.FUNCTION_VALUE_PARAMETERS) - paramsNode?.let { processParameters(it, parameters) } - } - - val functionSymbol = FunctionSymbol( - name = name, - location = createLocation(node, nameNode), - modifiers = modifiers, - containingScope = currentScope, - parameters = parameters, - typeParameters = typeParameters, - returnType = returnType, - receiverType = receiverType, - bodyScope = functionScope - ) - - currentScope.define(functionSymbol) - symbolTable.registerSymbol(functionSymbol) - withScope(functionScope, functionSymbol) { - val body = node.childByFieldName("body") - ?: node.findChild(SyntaxKind.FUNCTION_BODY) - ?: node.findChild(SyntaxKind.CONTROL_STRUCTURE_BODY) - processBody(body) - } - - return functionSymbol - } - - private fun processSecondaryConstructor(node: SyntaxNode): FunctionSymbol? { - val modifiers = extractModifiers(node) - - val constructorScope = currentScope.createChild( - kind = ScopeKind.CONSTRUCTOR, - range = node.range - ) - - val parameters = mutableListOf() - - withScope(constructorScope, null) { - val paramsNode = node.findChild(SyntaxKind.FUNCTION_VALUE_PARAMETERS) - paramsNode?.let { processParameters(it, parameters) } - } - - val constructorSymbol = FunctionSymbol( - name = "", - location = createLocation(node, node), - modifiers = modifiers, - containingScope = currentScope, - parameters = parameters, - isConstructor = true, - bodyScope = constructorScope - ) - - currentScope.define(constructorSymbol) - symbolTable.registerSymbol(constructorSymbol) - - return constructorSymbol - } - - private fun processInitBlock(node: SyntaxNode) { - val initScope = currentScope.createChild( - kind = ScopeKind.BLOCK, - range = node.range - ) - - withScope(initScope, null) { - processBody(node.findChild(SyntaxKind.STATEMENTS)) - } - } - - private fun processPropertyDeclaration(node: SyntaxNode): PropertySymbol? { - val multiVarDecl = node.findChild(SyntaxKind.MULTI_VARIABLE_DECLARATION) - if (multiVarDecl != null) { - processDestructuringDeclaration(node, multiVarDecl) - return null - } - - var nameNode = node.childByFieldName("name") - - if (nameNode == null) { - nameNode = node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - } - - if (nameNode == null) { - nameNode = extractPropertyName(node) - } - - if (nameNode == null) { - val children = node.children - val valVarIndex = children.indexOfFirst { it.kind == SyntaxKind.VAL || it.kind == SyntaxKind.VAR } - if (valVarIndex >= 0 && valVarIndex + 1 < children.size) { - val candidate = children[valVarIndex + 1] - val isValidIdentifier = candidate.text.matches(Regex("[a-zA-Z_][a-zA-Z0-9_]*")) - if (isValidIdentifier && (candidate.kind == SyntaxKind.UNKNOWN || candidate.kind == SyntaxKind.SIMPLE_IDENTIFIER)) { - nameNode = candidate - } - } - } - - if (nameNode == null) { - val multiVarFallback = node.children.find { it.text.startsWith("(") && it.text.endsWith(")") } - if (multiVarFallback != null) { - processDestructuringFromParenthesized(node, multiVarFallback) - return null - } - - val allIdentifiers = node.traverse().filter { it.kind == SyntaxKind.SIMPLE_IDENTIFIER }.toList() - if (allIdentifiers.isNotEmpty()) { - nameNode = allIdentifiers.first() - } - } - - if (nameNode == null) return null - - val name = nameNode.text - val modifiers = extractModifiers(node) - var typeNode = node.childByFieldName("type") - ?: node.findChild(SyntaxKind.NULLABLE_TYPE) - ?: node.findChild(SyntaxKind.USER_TYPE) - - if (typeNode == null) { - val varDecl = node.findChild(SyntaxKind.VARIABLE_DECLARATION) - if (varDecl != null) { - typeNode = varDecl.findChild(SyntaxKind.NULLABLE_TYPE) - ?: varDecl.findChild(SyntaxKind.USER_TYPE) - ?: varDecl.findChild(SyntaxKind.FUNCTION_TYPE) - } - } - - var type = extractType(typeNode) - val receiverType = extractReceiverType(node) - val isVar = node.children.any { it.kind == SyntaxKind.VAR } - val hasInitializer = node.childByFieldName("value") != null || - node.children.any { it.text == "=" } - val isDelegated = node.children.any { it.kind == SyntaxKind.BY } - - if (type == null && hasInitializer) { - val initializerExpr = findInitializerExpression(node) - type = inferTypeFromExpression(initializerExpr) - } - - val getter = node.findChild(SyntaxKind.GETTER)?.let { processAccessor(it, "get", receiverType) } - val setter = node.findChild(SyntaxKind.SETTER)?.let { processAccessor(it, "set", receiverType) } - - val propertySymbol = PropertySymbol( - name = name, - location = createLocation(node, nameNode), - modifiers = modifiers, - containingScope = currentScope, - type = type, - receiverType = receiverType, - getter = getter, - setter = setter, - isVar = isVar, - hasInitializer = hasInitializer, - isDelegated = isDelegated - ) - - currentScope.define(propertySymbol) - symbolTable.registerSymbol(propertySymbol) - - return propertySymbol - } - - private fun findInitializerExpression(node: SyntaxNode): SyntaxNode? { - val valueNode = node.childByFieldName("value") - if (valueNode != null) return valueNode - - val children = node.children - val equalsIndex = children.indexOfFirst { it.text == "=" } - if (equalsIndex >= 0 && equalsIndex + 1 < children.size) { - return children[equalsIndex + 1] - } - return null - } - - private fun inferTypeFromExpression(node: SyntaxNode?): TypeReference? { - node ?: return null - - return when (node.kind) { - SyntaxKind.INTEGER_LITERAL, - SyntaxKind.HEX_LITERAL, - SyntaxKind.BIN_LITERAL -> TypeReference.INT - - SyntaxKind.LONG_LITERAL -> TypeReference("Long") - - SyntaxKind.REAL_LITERAL -> { - val text = node.text - if (text.endsWith("f", ignoreCase = true)) { - TypeReference("Float") - } else { - TypeReference("Double") - } - } - - SyntaxKind.BOOLEAN_LITERAL -> TypeReference.BOOLEAN - - SyntaxKind.CHARACTER_LITERAL -> TypeReference("Char") - - SyntaxKind.STRING_LITERAL, - SyntaxKind.LINE_STRING_LITERAL, - SyntaxKind.MULTI_LINE_STRING_LITERAL -> TypeReference.STRING - - SyntaxKind.NULL_LITERAL -> TypeReference.ANY_NULLABLE - - SyntaxKind.COLLECTION_LITERAL -> inferCollectionLiteralType(node) - - SyntaxKind.CALL_EXPRESSION -> inferCallExpressionType(node) - - SyntaxKind.PARENTHESIZED_EXPRESSION -> { - val inner = node.namedChildren.firstOrNull() - inferTypeFromExpression(inner) - } - - SyntaxKind.IF_EXPRESSION -> inferIfExpressionType(node) - - SyntaxKind.WHEN_EXPRESSION -> inferWhenExpressionType(node) - - else -> { - val firstNamedChild = node.namedChildren.firstOrNull() - if (firstNamedChild != null && firstNamedChild.kind != node.kind) { - inferTypeFromExpression(firstNamedChild) - } else { - null - } - } - } - } - - private fun inferCollectionLiteralType(node: SyntaxNode): TypeReference { - val elements = node.namedChildren - if (elements.isEmpty()) { - return TypeReference.generic("List", TypeReference.ANY) - } - val firstElementType = inferTypeFromExpression(elements.first()) - return TypeReference.generic("List", firstElementType ?: TypeReference.ANY) - } - - private fun inferCallExpressionType(node: SyntaxNode): TypeReference? { - val calleeNode = node.namedChildren.firstOrNull() ?: return null - val calleeName = when (calleeNode.kind) { - SyntaxKind.SIMPLE_IDENTIFIER -> calleeNode.text - SyntaxKind.NAVIGATION_EXPRESSION -> { - calleeNode.namedChildren.lastOrNull()?.text - } - else -> calleeNode.text - } - - return when (calleeName) { - "listOf", "mutableListOf", "arrayListOf" -> { - val elementType = inferCallArgumentType(node) - TypeReference.generic("List", elementType ?: TypeReference.ANY) - } - "setOf", "mutableSetOf", "hashSetOf", "linkedSetOf" -> { - val elementType = inferCallArgumentType(node) - TypeReference.generic("Set", elementType ?: TypeReference.ANY) - } - "mapOf", "mutableMapOf", "hashMapOf", "linkedMapOf" -> { - TypeReference.generic("Map", TypeReference.ANY, TypeReference.ANY) - } - "arrayOf" -> { - val elementType = inferCallArgumentType(node) - TypeReference.generic("Array", elementType ?: TypeReference.ANY) - } - "intArrayOf" -> TypeReference("IntArray") - "longArrayOf" -> TypeReference("LongArray") - "floatArrayOf" -> TypeReference("FloatArray") - "doubleArrayOf" -> TypeReference("DoubleArray") - "booleanArrayOf" -> TypeReference("BooleanArray") - "charArrayOf" -> TypeReference("CharArray") - "byteArrayOf" -> TypeReference("ByteArray") - "shortArrayOf" -> TypeReference("ShortArray") - "emptyList" -> TypeReference.generic("List", TypeReference.ANY) - "emptySet" -> TypeReference.generic("Set", TypeReference.ANY) - "emptyMap" -> TypeReference.generic("Map", TypeReference.ANY, TypeReference.ANY) - else -> { - if (calleeName != null && calleeName.first().isUpperCase()) { - TypeReference(calleeName) - } else { - null - } - } - } - } - - private fun inferCallArgumentType(node: SyntaxNode): TypeReference? { - val callSuffix = node.findChild(SyntaxKind.CALL_SUFFIX) ?: return null - val valueArguments = callSuffix.findChild(SyntaxKind.VALUE_ARGUMENTS) ?: return null - val firstArg = valueArguments.namedChildren.firstOrNull() ?: return null - val argValue = firstArg.findChild(SyntaxKind.VALUE_ARGUMENT)?.namedChildren?.firstOrNull() - ?: firstArg.namedChildren.firstOrNull() - ?: return null - return inferTypeFromExpression(argValue) - } - - private fun inferIfExpressionType(node: SyntaxNode): TypeReference? { - val thenBranch = node.childByFieldName("consequence") - return inferTypeFromExpression(thenBranch?.namedChildren?.lastOrNull()) - } - - private fun inferWhenExpressionType(node: SyntaxNode): TypeReference? { - val firstEntry = node.findChild(SyntaxKind.WHEN_ENTRY) - val bodyNode = firstEntry?.childByFieldName("body") - return inferTypeFromExpression(bodyNode?.namedChildren?.lastOrNull()) - } - - private fun processDestructuringDeclaration(propertyNode: SyntaxNode, multiVarDecl: SyntaxNode) { - - val isVar = propertyNode.children.any { it.kind == SyntaxKind.VAR } - val modifiers = extractModifiers(propertyNode) - - val variableNodes = multiVarDecl.namedChildren.filter { - it.kind == SyntaxKind.VARIABLE_DECLARATION || - it.kind == SyntaxKind.SIMPLE_IDENTIFIER - } - - for ((index, varNode) in variableNodes.withIndex()) { - val nameNode = if (varNode.kind == SyntaxKind.SIMPLE_IDENTIFIER) { - varNode - } else { - varNode.findChild(SyntaxKind.SIMPLE_IDENTIFIER) ?: continue - } - - val name = nameNode.text - if (name == "_") continue - - val typeNode = varNode.findChild(SyntaxKind.USER_TYPE) - val type = extractType(typeNode) - - val propertySymbol = PropertySymbol( - name = name, - location = createLocation(varNode, nameNode), - modifiers = modifiers, - containingScope = currentScope, - type = type, - isVar = isVar, - hasInitializer = true - ) - - currentScope.define(propertySymbol) - symbolTable.registerSymbol(propertySymbol) - } - } - - private fun processDestructuringFromParenthesized(propertyNode: SyntaxNode, parenNode: SyntaxNode) { - val isVar = propertyNode.children.any { it.kind == SyntaxKind.VAR } - val modifiers = extractModifiers(propertyNode) - - val identifiers = parenNode.traverse() - .filter { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - .toList() - - for (nameNode in identifiers) { - val name = nameNode.text - if (name == "_") continue - - val propertySymbol = PropertySymbol( - name = name, - location = createLocation(nameNode, nameNode), - modifiers = modifiers, - containingScope = currentScope, - type = null, - isVar = isVar, - hasInitializer = true - ) - - currentScope.define(propertySymbol) - symbolTable.registerSymbol(propertySymbol) - } - } - - private fun extractPropertyName(node: SyntaxNode): SyntaxNode? { - for (child in node.namedChildren) { - if (child.kind == SyntaxKind.SIMPLE_IDENTIFIER) { - return child - } - } - return null - } - - private fun processAccessor(node: SyntaxNode, name: String, receiverType: TypeReference? = null): FunctionSymbol { - val modifiers = extractModifiers(node) - - val accessorScope = currentScope.createChild( - kind = ScopeKind.ACCESSOR, - range = node.range - ) - - val accessorSymbol = FunctionSymbol( - name = name, - location = createLocation(node, node), - modifiers = modifiers, - containingScope = currentScope, - bodyScope = accessorScope, - receiverType = receiverType - ) - - accessorScope.owner = accessorSymbol - - val body = node.childByFieldName("body") - ?: node.findChild(SyntaxKind.FUNCTION_BODY) - ?: node.findChild(SyntaxKind.CONTROL_STRUCTURE_BODY) - ?: node.namedChildren.find { it.kind != SyntaxKind.MODIFIER && it.kind != SyntaxKind.GET && it.kind != SyntaxKind.SET } - - if (body != null) { - withScope(accessorScope, accessorSymbol) { - processBody(body) - } - } - - return accessorSymbol - } - - private fun processTypeAlias(node: SyntaxNode): TypeAliasSymbol? { - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: node.findChild(SyntaxKind.TYPE_IDENTIFIER) - ?: return null - - val name = nameNode.text - val modifiers = extractModifiers(node) - val typeParameters = extractTypeParameters(node) - val underlyingType = extractType(node.childByFieldName("type")) - - val typeAliasSymbol = TypeAliasSymbol( - name = name, - location = createLocation(node, nameNode), - modifiers = modifiers, - containingScope = currentScope, - typeParameters = typeParameters, - underlyingType = underlyingType - ) - - currentScope.define(typeAliasSymbol) - symbolTable.registerSymbol(typeAliasSymbol) - - return typeAliasSymbol - } - - private fun processParameters(node: SyntaxNode, parameters: MutableList) { - var nextIsVararg = false - for (child in node.children) { - when { - child.kind == SyntaxKind.PARAMETER_MODIFIER && child.text == "vararg" -> { - nextIsVararg = true - } - child.text == "vararg" -> { - nextIsVararg = true - } - child.kind == SyntaxKind.PARAMETER || child.kind == SyntaxKind.CLASS_PARAMETER -> { - val param = processParameter(child, forceVararg = nextIsVararg) - if (param != null) { - parameters.add(param) - currentScope.define(param) - symbolTable.registerSymbol(param) - } - nextIsVararg = false - } - } - } - } - - private fun processParameter(node: SyntaxNode, forceVararg: Boolean = false): ParameterSymbol? { - val nameNode = node.childByFieldName("name") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: return null - - val name = nameNode.text - val modifiers = extractModifiers(node) - - val typeNode = node.childByFieldName("type") - var type = extractType(typeNode) - - if (type == null) { - val funcType = typeNode?.findChild(SyntaxKind.FUNCTION_TYPE) - ?: node.findChild(SyntaxKind.FUNCTION_TYPE) - if (funcType != null) { - type = extractType(funcType) - } - } - - if (type == null && typeNode != null) { - val userType = typeNode.findChild(SyntaxKind.USER_TYPE) - ?: typeNode.findChild(SyntaxKind.SIMPLE_USER_TYPE) - if (userType != null) { - type = extractType(userType) - } - } - - if (type == null) { - val userType = node.findChild(SyntaxKind.USER_TYPE) - ?: node.findChild(SyntaxKind.SIMPLE_USER_TYPE) - if (userType != null) { - type = extractType(userType) - } - } - - val hasDefault = node.childByFieldName("default_value") != null || - node.children.any { it.text == "=" } - - val isVararg = forceVararg || modifiers.isVararg - - return ParameterSymbol( - name = name, - location = createLocation(node, nameNode), - modifiers = modifiers, - containingScope = currentScope, - type = type, - hasDefaultValue = hasDefault, - isVararg = isVararg, - isCrossinline = modifiers.isCrossinline, - isNoinline = modifiers.isNoinline - ) - } - - private fun extractPrimaryConstructor(node: SyntaxNode, classScope: Scope): FunctionSymbol? { - val primaryConstructorNode = node.findChild(SyntaxKind.PRIMARY_CONSTRUCTOR) - val paramsNode = primaryConstructorNode?.findChild(SyntaxKind.CLASS_PARAMETER) - ?: node.findChild(SyntaxKind.CLASS_PARAMETER) - ?: return null - - val modifiers = primaryConstructorNode?.let { extractModifiers(it) } ?: Modifiers.EMPTY - - val parameters = mutableListOf() - - val tempScope = currentScope - currentScope = classScope - - val actualParamsNode = primaryConstructorNode ?: node - for (child in actualParamsNode.namedChildren) { - if (child.kind == SyntaxKind.CLASS_PARAMETER) { - val param = processParameter(child) - if (param != null) { - parameters.add(param) - classScope.define(param) - symbolTable.registerSymbol(param) - - if (child.children.any { it.kind == SyntaxKind.VAL || it.kind == SyntaxKind.VAR }) { - val isVar = child.children.any { it.kind == SyntaxKind.VAR } - val property = PropertySymbol( - name = param.name, - location = param.location, - modifiers = param.modifiers, - containingScope = classScope, - type = param.type, - isVar = isVar, - hasInitializer = true - ) - classScope.define(property) - symbolTable.registerSymbol(property) - } - } - } - } - - currentScope = tempScope - - return FunctionSymbol( - name = "", - location = createLocation(primaryConstructorNode ?: node, primaryConstructorNode ?: node), - modifiers = modifiers, - containingScope = classScope, - parameters = parameters, - isConstructor = true, - isPrimaryConstructor = true - ) - } - - private fun processBody(node: SyntaxNode?) { - node ?: return - - val children = when (node.kind) { - SyntaxKind.FUNCTION_BODY -> { - val statements = node.findChild(SyntaxKind.STATEMENTS) - statements?.namedChildren ?: node.namedChildren - } - SyntaxKind.CONTROL_STRUCTURE_BODY -> { - val statements = node.findChild(SyntaxKind.STATEMENTS) - statements?.namedChildren ?: node.namedChildren - } - else -> node.namedChildren - } - - for (child in children) { - when (child.kind) { - SyntaxKind.PROPERTY_DECLARATION -> { - processPropertyDeclaration(child) - } - SyntaxKind.FOR_STATEMENT -> processForStatement(child) - SyntaxKind.IF_EXPRESSION -> processIfExpression(child) - SyntaxKind.WHEN_EXPRESSION -> processWhenExpression(child) - SyntaxKind.TRY_EXPRESSION -> processTryExpression(child) - SyntaxKind.LAMBDA_LITERAL -> processLambda(child) - SyntaxKind.ERROR -> processBody(child) - else -> processBody(child) - } - } - } - - private fun processForStatement(node: SyntaxNode) { - val loopScope = currentScope.createChild(ScopeKind.FOR_LOOP, range = node.range) - - withScope(loopScope, null) { - val loopVariable = node.childByFieldName("loop_parameter") - ?: node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - - if (loopVariable != null) { - val param = ParameterSymbol( - name = loopVariable.text, - location = createLocation(loopVariable, loopVariable), - modifiers = Modifiers.EMPTY, - containingScope = currentScope - ) - currentScope.define(param) - symbolTable.registerSymbol(param) - } - - processBody(node.childByFieldName("body")) - } - } - - private fun processIfExpression(node: SyntaxNode) { - val thenBranch = node.childByFieldName("consequence") - val elseBranch = node.childByFieldName("alternative") - - if (thenBranch != null) { - val thenScope = currentScope.createChild(ScopeKind.BLOCK, range = thenBranch.range) - withScope(thenScope, null) { - processBody(thenBranch) - } - } - - if (elseBranch != null) { - val elseScope = currentScope.createChild(ScopeKind.BLOCK, range = elseBranch.range) - withScope(elseScope, null) { - processBody(elseBranch) - } - } - } - - private fun processWhenExpression(node: SyntaxNode) { - for (entry in node.findChildren(SyntaxKind.WHEN_ENTRY)) { - val entryScope = currentScope.createChild(ScopeKind.WHEN_ENTRY, range = entry.range) - withScope(entryScope, null) { - processBody(entry.childByFieldName("body")) - } - } - } - - private fun processTryExpression(node: SyntaxNode) { - val tryBody = node.childByFieldName("body") - if (tryBody != null) { - val tryScope = currentScope.createChild(ScopeKind.BLOCK, range = tryBody.range) - withScope(tryScope, null) { - processBody(tryBody) - } - } - - for (catchBlock in node.findChildren(SyntaxKind.CATCH_BLOCK)) { - val catchScope = currentScope.createChild(ScopeKind.CATCH, range = catchBlock.range) - withScope(catchScope, null) { - val exceptionParam = catchBlock.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - if (exceptionParam != null) { - val param = ParameterSymbol( - name = exceptionParam.text, - location = createLocation(exceptionParam, exceptionParam), - modifiers = Modifiers.EMPTY, - containingScope = currentScope - ) - currentScope.define(param) - symbolTable.registerSymbol(param) - } - processBody(catchBlock.childByFieldName("body")) - } - } - - val finallyBlock = node.findChild(SyntaxKind.FINALLY_BLOCK) - if (finallyBlock != null) { - val finallyScope = currentScope.createChild(ScopeKind.BLOCK, range = finallyBlock.range) - withScope(finallyScope, null) { - processBody(finallyBlock.childByFieldName("body")) - } - } - } - - private fun processLambda(node: SyntaxNode) { - val lambdaScope = currentScope.createChild(ScopeKind.LAMBDA, range = node.range) - - withScope(lambdaScope, null) { - val params = node.findChild(SyntaxKind.LAMBDA_PARAMETERS) - if (params != null) { - for (child in params.namedChildren) { - if (child.kind == SyntaxKind.PARAMETER || - child.kind == SyntaxKind.SIMPLE_IDENTIFIER) { - val nameNode = if (child.kind == SyntaxKind.PARAMETER) { - child.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - } else { - child - } - if (nameNode != null) { - val param = ParameterSymbol( - name = nameNode.text, - location = createLocation(child, nameNode), - modifiers = Modifiers.EMPTY, - containingScope = currentScope - ) - currentScope.define(param) - symbolTable.registerSymbol(param) - } - } - } - } - - processBody(node.findChild(SyntaxKind.STATEMENTS)) - } - } - - private fun extractModifiers(node: SyntaxNode): Modifiers { - val modifiersNode = node.findChild(SyntaxKind.MODIFIERS) ?: return Modifiers.EMPTY - - var visibility = Visibility.DEFAULT - var modality = Modality.DEFAULT - var isInline = false - var isSuspend = false - var isTailrec = false - var isOperator = false - var isInfix = false - var isExternal = false - var isConst = false - var isLateInit = false - var isData = false - var isValue = false - var isInner = false - var isCompanion = false - var isAnnotation = false - var isEnum = false - var isVararg = false - var isCrossinline = false - var isNoinline = false - var isReified = false - var isOverride = false - var isExpect = false - var isActual = false - - for (child in modifiersNode.traverse()) { - when (child.kind) { - SyntaxKind.VISIBILITY_MODIFIER -> { - visibility = when (child.text) { - "public" -> Visibility.PUBLIC - "private" -> Visibility.PRIVATE - "protected" -> Visibility.PROTECTED - "internal" -> Visibility.INTERNAL - else -> Visibility.DEFAULT - } - } - SyntaxKind.INHERITANCE_MODIFIER -> { - modality = when (child.text) { - "final" -> Modality.FINAL - "open" -> Modality.OPEN - "abstract" -> Modality.ABSTRACT - "sealed" -> Modality.SEALED - else -> Modality.DEFAULT - } - } - SyntaxKind.FUNCTION_MODIFIER -> { - when (child.text) { - "inline" -> isInline = true - "suspend" -> isSuspend = true - "tailrec" -> isTailrec = true - "operator" -> isOperator = true - "infix" -> isInfix = true - "external" -> isExternal = true - } - } - SyntaxKind.PROPERTY_MODIFIER -> { - if (child.text == "const") isConst = true - } - SyntaxKind.MEMBER_MODIFIER -> { - when (child.text) { - "override" -> isOverride = true - "lateinit" -> isLateInit = true - } - } - SyntaxKind.CLASS_MODIFIER -> { - when (child.text) { - "data" -> isData = true - "value" -> isValue = true - "inner" -> isInner = true - "annotation" -> isAnnotation = true - "enum" -> isEnum = true - "sealed" -> modality = Modality.SEALED - } - } - SyntaxKind.PARAMETER_MODIFIER -> { - when (child.text) { - "vararg" -> isVararg = true - "crossinline" -> isCrossinline = true - "noinline" -> isNoinline = true - } - } - SyntaxKind.TYPE_PARAMETER_MODIFIER -> { - if (child.text == "reified") isReified = true - } - SyntaxKind.PLATFORM_MODIFIER -> { - when (child.text) { - "expect" -> isExpect = true - "actual" -> isActual = true - } - } - else -> {} - } - } - - return Modifiers( - visibility = visibility, - modality = modality, - isInline = isInline, - isSuspend = isSuspend, - isTailrec = isTailrec, - isOperator = isOperator, - isInfix = isInfix, - isExternal = isExternal, - isConst = isConst, - isLateInit = isLateInit, - isData = isData, - isValue = isValue, - isInner = isInner, - isCompanion = isCompanion, - isAnnotation = isAnnotation, - isEnum = isEnum, - isVararg = isVararg, - isCrossinline = isCrossinline, - isNoinline = isNoinline, - isReified = isReified, - isOverride = isOverride, - isExpect = isExpect, - isActual = isActual - ) - } - - private fun extractTypeParameters(node: SyntaxNode): List { - val typeParamsNode = node.findChild(SyntaxKind.TYPE_PARAMETERS) ?: return emptyList() - - return typeParamsNode.namedChildren - .filter { it.kind == SyntaxKind.TYPE_PARAMETER } - .mapNotNull { paramNode -> - val nameNode = paramNode.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: paramNode.findChild(SyntaxKind.TYPE_IDENTIFIER) - ?: return@mapNotNull null - - val bounds = extractTypeBounds(paramNode) - val variance = extractVariance(paramNode) - val isReified = paramNode.traverse().any { it.text == "reified" } - - TypeParameterSymbol( - name = nameNode.text, - location = createLocation(paramNode, nameNode), - modifiers = Modifiers(isReified = isReified), - containingScope = currentScope, - bounds = bounds, - variance = variance, - isReified = isReified - ) - } - } - - private fun extractTypeBounds(node: SyntaxNode): List { - val bounds = mutableListOf() - - for (child in node.namedChildren) { - if (child.kind == SyntaxKind.USER_TYPE || - child.kind == SyntaxKind.NULLABLE_TYPE) { - val type = extractType(child) - if (type != null) bounds.add(type) - } - } - - return bounds - } - - private fun extractVariance(node: SyntaxNode): Variance { - for (child in node.children) { - if (child.kind == SyntaxKind.VARIANCE_MODIFIER) { - return Variance.fromKeyword(child.text) - } - if (child.text == "in") return Variance.IN - if (child.text == "out") return Variance.OUT - } - return Variance.INVARIANT - } - - private fun extractSuperTypes(node: SyntaxNode): List { - - val results = mutableListOf() - - val delegationSpecifiers = node.findChild(SyntaxKind.DELEGATION_SPECIFIERS) - if (delegationSpecifiers != null) { - delegationSpecifiers.namedChildren - .filter { it.kind == SyntaxKind.DELEGATION_SPECIFIER || - it.kind == SyntaxKind.ANNOTATED_DELEGATION_SPECIFIER || - it.kind == SyntaxKind.CONSTRUCTOR_INVOCATION || - it.kind == SyntaxKind.USER_TYPE } - .mapNotNull { spec -> extractTypeFromDelegationSpecifier(spec) } - .let { results.addAll(it) } - } - - val directSpecifiers = node.children.filter { - it.kind == SyntaxKind.DELEGATION_SPECIFIER || - it.kind == SyntaxKind.ANNOTATED_DELEGATION_SPECIFIER || - it.kind == SyntaxKind.CONSTRUCTOR_INVOCATION - } - - if (directSpecifiers.isNotEmpty()) { - directSpecifiers.mapNotNull { spec -> extractTypeFromDelegationSpecifier(spec) } - .let { results.addAll(it) } - } - - return results - } - - private fun extractTypeFromDelegationSpecifier(spec: SyntaxNode): TypeReference? { - val typeNode = spec.findChild(SyntaxKind.USER_TYPE) - ?: spec.findChild(SyntaxKind.CONSTRUCTOR_INVOCATION)?.findChild(SyntaxKind.USER_TYPE) - ?: if (spec.kind == SyntaxKind.USER_TYPE) spec else null - - if (typeNode == null) { - for (child in spec.children) { - if (child.kind == SyntaxKind.USER_TYPE || child.kind == SyntaxKind.SIMPLE_USER_TYPE) { - return extractType(child) - } - if (child.kind == SyntaxKind.SIMPLE_IDENTIFIER || child.kind == SyntaxKind.TYPE_IDENTIFIER) { - return TypeReference(child.text, range = child.range) - } - } - val firstIdent = spec.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: spec.findChild(SyntaxKind.TYPE_IDENTIFIER) - if (firstIdent != null) { - return TypeReference(firstIdent.text, range = firstIdent.range) - } - } - - return extractType(typeNode) - } - - private fun extractReceiverType(node: SyntaxNode): TypeReference? { - val receiverNode = node.findChild(SyntaxKind.RECEIVER_TYPE) - - if (receiverNode != null) { - return extractType( - receiverNode.findChild(SyntaxKind.USER_TYPE) - ?: receiverNode.findChild(SyntaxKind.NULLABLE_TYPE) - ?: receiverNode.findChild(SyntaxKind.PARENTHESIZED_TYPE) - ?: receiverNode.namedChildren.firstOrNull() - ) - } - - for (child in node.namedChildren) { - if (child.kind != SyntaxKind.USER_TYPE && child.kind != SyntaxKind.SIMPLE_USER_TYPE) continue - val nextSibling = node.namedChildren.getOrNull(node.namedChildren.indexOf(child) + 1) - if (nextSibling?.kind != SyntaxKind.SIMPLE_IDENTIFIER && nextSibling?.text != ".") continue - val type = extractType(child) - if (type != null) return type - } - - val children = node.children - for (i in children.indices) { - if (children[i].text != "." || i == 0) continue - val prevChild = children[i - 1] - if (prevChild.kind == SyntaxKind.USER_TYPE || - prevChild.kind == SyntaxKind.SIMPLE_USER_TYPE || - prevChild.kind == SyntaxKind.NULLABLE_TYPE - ) { - val type = extractType(prevChild) - if (type != null) return type - } - if (prevChild.kind == SyntaxKind.SIMPLE_IDENTIFIER || prevChild.kind == SyntaxKind.TYPE_IDENTIFIER) { - return TypeReference(prevChild.text, range = prevChild.range) - } - } - - return null - } - - private fun extractReturnType(node: SyntaxNode): TypeReference? { - val typeNode = node.childByFieldName("return_type") - ?: node.namedChildren.find { - it.kind == SyntaxKind.USER_TYPE || - it.kind == SyntaxKind.NULLABLE_TYPE || - it.kind == SyntaxKind.FUNCTION_TYPE - } - - return extractType(typeNode) - } - - private fun extractType(node: SyntaxNode?): TypeReference? { - node ?: return null - - return try { - when (node.kind) { - SyntaxKind.USER_TYPE, - SyntaxKind.SIMPLE_USER_TYPE -> { - val name = extractTypeName(node) - if (name.isEmpty()) return null - val typeArgs = extractTypeArguments(node) - TypeReference(name, typeArgs, range = node.range) - } - - SyntaxKind.NULLABLE_TYPE -> { - val inner = node.namedChildren.firstOrNull() - val innerType = extractType(inner) - innerType?.nullable() - } - - SyntaxKind.FUNCTION_TYPE -> { - extractFunctionType(node) - } - - SyntaxKind.PARENTHESIZED_TYPE -> { - extractType(node.namedChildren.firstOrNull()) - } - - else -> null - } - } catch (e: Exception) { - null - } - } - - private fun extractTypeName(node: SyntaxNode): String { - val parts = mutableListOf() - - fun collectParts(n: SyntaxNode) { - for (child in n.namedChildren) { - when (child.kind) { - SyntaxKind.SIMPLE_USER_TYPE -> { - val id = child.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: child.findChild(SyntaxKind.TYPE_IDENTIFIER) - if (id != null) parts.add(id.text) - } - SyntaxKind.SIMPLE_IDENTIFIER, - SyntaxKind.TYPE_IDENTIFIER -> { - parts.add(child.text) - } - else -> collectParts(child) - } - } - } - - val directId = node.findChild(SyntaxKind.SIMPLE_IDENTIFIER) - ?: node.findChild(SyntaxKind.TYPE_IDENTIFIER) - if (directId != null && node.kind == SyntaxKind.SIMPLE_USER_TYPE) { - parts.add(directId.text) - } else { - collectParts(node) - } - - return parts.joinToString(".") - } - - private fun extractTypeArguments(node: SyntaxNode): List { - val typeArgsNode = node.findChild(SyntaxKind.TYPE_ARGUMENTS) - ?: return emptyList() - - return typeArgsNode.namedChildren - .filter { it.kind == SyntaxKind.TYPE_PROJECTION } - .mapNotNull { proj -> - extractType(proj.namedChildren.firstOrNull()) - } - } - - private fun extractFunctionType(node: SyntaxNode): TypeReference { - - val receiverType = node.findChild(SyntaxKind.RECEIVER_TYPE)?.let { - extractType(it.namedChildren.firstOrNull()) - } - - val paramTypes = node.findChild(SyntaxKind.FUNCTION_TYPE_PARAMETERS) - ?.namedChildren - ?.mapNotNull { extractType(it) } - ?: emptyList() - - val returnType = node.namedChildren.lastOrNull()?.let { extractType(it) } - ?: TypeReference.UNIT - - val result = TypeReference.functionType( - receiverType = receiverType, - parameterTypes = paramTypes, - returnType = returnType, - range = node.range - ) - return result - } - - private fun extractQualifiedName(node: SyntaxNode?): String? { - node ?: return null - - return when (node.kind) { - SyntaxKind.IDENTIFIER -> { - node.namedChildren - .filter { it.kind == SyntaxKind.SIMPLE_IDENTIFIER } - .joinToString(".") { it.text } - } - SyntaxKind.SIMPLE_IDENTIFIER -> node.text - else -> { - val parts = mutableListOf() - for (child in node.traverse()) { - if (child.kind == SyntaxKind.SIMPLE_IDENTIFIER) { - parts.add(child.text) - } - } - if (parts.isEmpty()) null else parts.joinToString(".") - } - } - } - - private fun determineClassKind(node: SyntaxNode, modifiers: Modifiers): ClassKind { - return when { - modifiers.isEnum -> ClassKind.ENUM_CLASS - modifiers.isAnnotation -> ClassKind.ANNOTATION_CLASS - modifiers.isData -> ClassKind.DATA_CLASS - modifiers.isValue -> ClassKind.VALUE_CLASS - else -> ClassKind.CLASS - } - } - - private fun createLocation(node: SyntaxNode, nameNode: SyntaxNode?): SymbolLocation { - return SymbolLocation( - filePath = filePath, - range = node.range, - nameRange = nameNode?.range ?: node.range - ) - } - - private inline fun withScope(scope: Scope, owner: Symbol?, block: () -> Unit) { - val previousScope = currentScope - currentScope = scope - if (owner != null) { - scope.owner = owner - } - try { - block() - } finally { - currentScope = previousScope - } - } - - companion object { - private val CLASS_PATTERN = Regex( - """(?:public\s+|private\s+|internal\s+|protected\s+|open\s+|abstract\s+|sealed\s+|data\s+|inner\s+)*class\s+([A-Za-z_][A-Za-z0-9_]*)""" - ) - private val INTERFACE_PATTERN = Regex( - """(?:public\s+|private\s+|internal\s+|protected\s+|sealed\s+)*interface\s+([A-Za-z_][A-Za-z0-9_]*)""" - ) - private val OBJECT_PATTERN = Regex( - """(?:public\s+|private\s+|internal\s+|protected\s+)*object\s+([A-Za-z_][A-Za-z0-9_]*)""" - ) - private val SUPERTYPE_PATTERN = Regex("""([A-Za-z_][A-Za-z0-9_.]*)\s*(?:\([^)]*\))?""") - - fun build(tree: SyntaxTree, filePath: String): SymbolTable { - return SymbolBuilder(tree, filePath).build() - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolLocation.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolLocation.kt deleted file mode 100644 index a33c580220..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolLocation.kt +++ /dev/null @@ -1,111 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -import org.appdevforall.codeonthego.lsp.kotlin.parser.Position -import org.appdevforall.codeonthego.lsp.kotlin.parser.TextRange - -/** - * Location of a symbol declaration in source code. - * - * This class captures where a symbol is defined, including: - * - The file path containing the declaration - * - The text range covering the entire declaration - * - The text range covering just the symbol's name (for go-to-definition) - * - * ## Usage - * - * ```kotlin - * val location = SymbolLocation( - * filePath = "/src/main/kotlin/Foo.kt", - * range = TextRange(Position(0, 0), Position(5, 1)), - * nameRange = TextRange(Position(0, 6), Position(0, 9)) - * ) - * - * // For "class Foo { ... }" - * // range covers "class Foo { ... }" - * // nameRange covers "Foo" - * ``` - * - * ## Synthetic Symbols - * - * For symbols that don't have a source location (e.g., stdlib symbols loaded from index), - * use [SYNTHETIC] which has empty ranges. - * - * @property filePath Absolute path to the source file - * @property range Text range covering the entire declaration - * @property nameRange Text range covering the symbol's name (for precise navigation) - */ -data class SymbolLocation( - val filePath: String, - val range: TextRange, - val nameRange: TextRange -) { - /** - * The start position of the declaration. - */ - val startPosition: Position get() = range.start - - /** - * The end position of the declaration. - */ - val endPosition: Position get() = range.end - - /** - * The position of the symbol's name (for go-to-definition). - */ - val namePosition: Position get() = nameRange.start - - /** - * Whether this location is from actual source code. - */ - val isFromSource: Boolean get() = filePath.isNotEmpty() - - /** - * Whether this is a synthetic location (no source). - */ - val isSynthetic: Boolean get() = !isFromSource - - /** - * Returns a display string for error messages and hover info. - */ - fun toDisplayString(): String { - return if (isFromSource) { - "$filePath:${range.start.displayLine}:${range.start.displayColumn}" - } else { - "" - } - } - - override fun toString(): String = toDisplayString() - - companion object { - /** - * Location for symbols without source (stdlib, synthetic). - */ - val SYNTHETIC: SymbolLocation = SymbolLocation( - filePath = "", - range = TextRange.EMPTY, - nameRange = TextRange.EMPTY - ) - - /** - * Creates a location from a file path and position. - * - * @param filePath Path to the source file - * @param range Range covering the declaration - * @param nameRange Range covering just the name - */ - fun of( - filePath: String, - range: TextRange, - nameRange: TextRange = range - ): SymbolLocation = SymbolLocation(filePath, range, nameRange) - - /** - * Creates a location covering a single position (point). - */ - fun at(filePath: String, position: Position): SymbolLocation { - val range = TextRange(position, position) - return SymbolLocation(filePath, range, range) - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolTable.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolTable.kt deleted file mode 100644 index 0a71df37d3..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolTable.kt +++ /dev/null @@ -1,329 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -import org.appdevforall.codeonthego.lsp.kotlin.parser.Position -import org.appdevforall.codeonthego.lsp.kotlin.parser.SyntaxTree -import org.appdevforall.codeonthego.lsp.kotlin.parser.TextRange - -/** - * Per-file symbol storage and lookup. - * - * SymbolTable holds all symbols extracted from a single Kotlin source file. - * It provides efficient lookup by: - * - Name (with scope-aware resolution) - * - Position (for go-to-definition) - * - Kind (for document symbols) - * - * ## Usage - * - * ```kotlin - * val parser = KotlinParser() - * val result = parser.parse(source) - * val table = SymbolBuilder.build(result.tree, filePath) - * - * // Find symbol at cursor - * val symbol = table.symbolAt(Position(10, 5)) - * - * // Find all functions - * val functions = table.functions - * ``` - * - * @property filePath The source file path - * @property packageName The package name declared in this file - * @property fileScope The root scope for this file - * @property syntaxTree The parsed syntax tree (for source text access) - */ -class SymbolTable( - val filePath: String, - val packageName: String, - val fileScope: Scope, - val syntaxTree: SyntaxTree? = null -) { - private val symbolsByRange: MutableMap = mutableMapOf() - private val referencesByPosition: MutableMap = mutableMapOf() - - /** - * All top-level symbols in this file. - */ - val topLevelSymbols: List - get() = fileScope.allSymbols - - /** - * All classes declared in this file. - */ - val classes: List - get() = fileScope.findAll() - - /** - * All top-level functions in this file. - */ - val functions: List - get() = fileScope.findAll() - - /** - * All top-level properties in this file. - */ - val properties: List - get() = fileScope.findAll() - - /** - * All type aliases in this file. - */ - val typeAliases: List - get() = fileScope.findAll() - - /** - * Total number of symbols in this file. - */ - val symbolCount: Int - get() = countSymbols(fileScope) - - /** - * Imports declared in this file. - */ - var imports: List = emptyList() - internal set - - /** - * Registers a symbol for range-based lookup. - */ - fun registerSymbol(symbol: Symbol) { - if (!symbol.location.isSynthetic) { - symbolsByRange[symbol.location.range] = symbol - } - } - - /** - * Registers a symbol reference for position-based lookup. - */ - fun registerReference(position: Position, reference: SymbolReference) { - referencesByPosition[position] = reference - } - - /** - * Finds the symbol declared at a position. - * - * @param position The cursor position - * @return The symbol at that position, or null - */ - fun symbolAt(position: Position): Symbol? { - return symbolsByRange.entries - .filter { (range, _) -> position in range } - .minByOrNull { (range, _) -> range.length } - ?.value - } - - /** - * Finds the symbol whose name is at a position. - * - * More precise than [symbolAt] - checks the name range specifically. - */ - fun symbolNameAt(position: Position): Symbol? { - return symbolsByRange.values - .filter { symbol -> position in symbol.location.nameRange } - .minByOrNull { symbol -> symbol.location.nameRange.length } - } - - /** - * Finds all symbols overlapping a range. - */ - fun symbolsInRange(range: TextRange): List { - return symbolsByRange.entries - .filter { it.key.overlaps(range) } - .map { it.value } - } - - /** - * Resolves a name at a specific position (considering scope). - * - * @param name The name to resolve - * @param position The position for scope context - * @return List of symbols matching the name - */ - fun resolve(name: String, position: Position): List { - val scope = scopeAt(position) ?: fileScope - return scope.resolve(name) - } - - /** - * Finds the scope containing a position. - */ - fun scopeAt(position: Position): Scope? { - return findScopeAt(fileScope, position) - } - - /** - * Gets all symbols visible at a position. - * - * This includes symbols from the current scope and all parent scopes. - */ - fun allVisibleSymbols(position: Position): List { - val scope = scopeAt(position) ?: fileScope - val symbols = scope.collectAll { true } - return symbols - } - - /** - * Gets all symbols for document outline. - * - * Returns symbols in a hierarchical structure suitable for - * displaying in an outline view. - */ - fun documentSymbols(): List { - return topLevelSymbols.mapNotNull { toDocumentSymbol(it) } - } - - /** - * Finds all references to a symbol within this file. - */ - fun findReferences(symbol: Symbol): List { - return referencesByPosition.values.filter { it.resolvedSymbol == symbol } - } - - private fun findScopeAt(scope: Scope, position: Position): Scope? { - if (position !in scope.range && scope.range != TextRange.EMPTY) { - return null - } - - for (child in scope.children) { - val found = findScopeAt(child, position) - if (found != null) return found - } - - return scope - } - - private fun countSymbols(scope: Scope): Int { - var count = scope.symbolCount - for (child in scope.children) { - count += countSymbols(child) - } - return count - } - - private fun toDocumentSymbol(symbol: Symbol): DocumentSymbol? { - val children = when (symbol) { - is ClassSymbol -> symbol.members.mapNotNull { toDocumentSymbol(it) } - is FunctionSymbol -> emptyList() - is PropertySymbol -> emptyList() - else -> emptyList() - } - - return DocumentSymbol( - name = symbol.name, - kind = symbol.toDocumentSymbolKind(), - range = symbol.location.range, - selectionRange = symbol.location.nameRange, - children = children - ) - } - - override fun toString(): String { - return "SymbolTable($filePath, package=$packageName, symbols=$symbolCount)" - } -} - -/** - * Document symbol for outline view. - */ -data class DocumentSymbol( - val name: String, - val kind: DocumentSymbolKind, - val range: TextRange, - val selectionRange: TextRange, - val children: List = emptyList(), - val detail: String? = null -) - -/** - * Kind of document symbol (for LSP symbolKind). - */ -enum class DocumentSymbolKind { - FILE, - MODULE, - NAMESPACE, - PACKAGE, - CLASS, - METHOD, - PROPERTY, - FIELD, - CONSTRUCTOR, - ENUM, - INTERFACE, - FUNCTION, - VARIABLE, - CONSTANT, - STRING, - NUMBER, - BOOLEAN, - ARRAY, - OBJECT, - KEY, - NULL, - ENUM_MEMBER, - STRUCT, - EVENT, - OPERATOR, - TYPE_PARAMETER -} - -/** - * Reference to a symbol at a specific location. - */ -data class SymbolReference( - val range: TextRange, - val name: String, - val resolvedSymbol: Symbol?, - val kind: ReferenceKind = ReferenceKind.READ -) - -/** - * Kind of symbol reference. - */ -enum class ReferenceKind { - READ, - WRITE, - CALL, - TYPE, - IMPORT -} - -/** - * Import information. - */ -data class ImportInfo( - val fqName: String, - val alias: String?, - val isStar: Boolean, - val range: TextRange -) { - val simpleName: String - get() = alias ?: fqName.substringAfterLast('.') - - val packageName: String - get() = if (isStar) fqName else fqName.substringBeforeLast('.', "") -} - -/** - * Extension to get DocumentSymbolKind for a symbol. - */ -fun Symbol.toDocumentSymbolKind(): DocumentSymbolKind = when (this) { - is ClassSymbol -> when { - isInterface -> DocumentSymbolKind.INTERFACE - isEnum -> DocumentSymbolKind.ENUM - isObject -> DocumentSymbolKind.OBJECT - else -> DocumentSymbolKind.CLASS - } - is FunctionSymbol -> when { - isConstructor -> DocumentSymbolKind.CONSTRUCTOR - isOperator -> DocumentSymbolKind.OPERATOR - else -> DocumentSymbolKind.FUNCTION - } - is PropertySymbol -> when { - isConst -> DocumentSymbolKind.CONSTANT - else -> DocumentSymbolKind.PROPERTY - } - is ParameterSymbol -> DocumentSymbolKind.VARIABLE - is TypeParameterSymbol -> DocumentSymbolKind.TYPE_PARAMETER - is TypeAliasSymbol -> DocumentSymbolKind.CLASS - is PackageSymbol -> DocumentSymbolKind.PACKAGE -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolVisitor.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolVisitor.kt deleted file mode 100644 index 5d216b3bcc..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/SymbolVisitor.kt +++ /dev/null @@ -1,136 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -/** - * Visitor pattern interface for traversing symbol hierarchies. - * - * Implement this interface to process different kinds of symbols uniformly. - * The visitor pattern allows adding new operations without modifying symbol classes. - * - * ## Type Parameters - * - * - [R]: The return type of visit methods - * - [D]: Additional data passed to visit methods - * - * ## Example - * - * ```kotlin - * class SymbolPrinter : SymbolVisitor { - * override fun visitClass(symbol: ClassSymbol, indent: Int) { - * println(" ".repeat(indent) + symbol.name) - * symbol.members.forEach { it.accept(this, indent + 1) } - * } - * - * override fun visitFunction(symbol: FunctionSymbol, indent: Int) { - * println(" ".repeat(indent) + symbol.name + "()") - * } - * // ... other visit methods - * } - * - * symbol.accept(SymbolPrinter(), 0) - * ``` - * - * @see Symbol - * @see SymbolVisitorBase - */ -interface SymbolVisitor { - fun visitClass(symbol: ClassSymbol, data: D): R - fun visitFunction(symbol: FunctionSymbol, data: D): R - fun visitProperty(symbol: PropertySymbol, data: D): R - fun visitParameter(symbol: ParameterSymbol, data: D): R - fun visitTypeParameter(symbol: TypeParameterSymbol, data: D): R - fun visitTypeAlias(symbol: TypeAliasSymbol, data: D): R - fun visitPackage(symbol: PackageSymbol, data: D): R -} - -/** - * Base implementation of [SymbolVisitor] with default behavior. - * - * All visit methods delegate to [visitSymbol] by default. - * Override specific methods to handle particular symbol types. - * - * @see SymbolVisitor - */ -abstract class SymbolVisitorBase : SymbolVisitor { - /** - * Default handler for all symbols. - * Override this to provide a catch-all implementation. - */ - abstract fun visitSymbol(symbol: Symbol, data: D): R - - override fun visitClass(symbol: ClassSymbol, data: D): R = visitSymbol(symbol, data) - override fun visitFunction(symbol: FunctionSymbol, data: D): R = visitSymbol(symbol, data) - override fun visitProperty(symbol: PropertySymbol, data: D): R = visitSymbol(symbol, data) - override fun visitParameter(symbol: ParameterSymbol, data: D): R = visitSymbol(symbol, data) - override fun visitTypeParameter(symbol: TypeParameterSymbol, data: D): R = visitSymbol(symbol, data) - override fun visitTypeAlias(symbol: TypeAliasSymbol, data: D): R = visitSymbol(symbol, data) - override fun visitPackage(symbol: PackageSymbol, data: D): R = visitSymbol(symbol, data) -} - -/** - * Visitor that returns Unit and takes no data. - * - * Useful for side-effecting operations like printing or collecting. - */ -abstract class SymbolVoidVisitor : SymbolVisitor { - abstract fun visitSymbol(symbol: Symbol) - - override fun visitClass(symbol: ClassSymbol, data: Unit) = visitSymbol(symbol) - override fun visitFunction(symbol: FunctionSymbol, data: Unit) = visitSymbol(symbol) - override fun visitProperty(symbol: PropertySymbol, data: Unit) = visitSymbol(symbol) - override fun visitParameter(symbol: ParameterSymbol, data: Unit) = visitSymbol(symbol) - override fun visitTypeParameter(symbol: TypeParameterSymbol, data: Unit) = visitSymbol(symbol) - override fun visitTypeAlias(symbol: TypeAliasSymbol, data: Unit) = visitSymbol(symbol) - override fun visitPackage(symbol: PackageSymbol, data: Unit) = visitSymbol(symbol) -} - -/** - * Collects symbols into a list using the visitor pattern. - */ -class SymbolCollector( - private val predicate: (Symbol, D) -> Boolean = { _, _ -> true } -) : SymbolVisitorBase, D>() { - private val collected = mutableListOf() - - override fun visitSymbol(symbol: Symbol, data: D): List { - if (predicate(symbol, data)) { - collected.add(symbol) - } - return collected - } - - fun result(): List = collected.toList() - - companion object { - fun collectAll(symbols: Iterable, data: D): List { - val collector = SymbolCollector() - symbols.forEach { it.accept(collector, data) } - return collector.result() - } - } -} - -/** - * Extension function to visit a symbol without data. - */ -fun Symbol.accept(visitor: SymbolVisitor): R = accept(visitor, Unit) - -/** - * Extension function to traverse all symbols in a scope. - */ -fun Scope.visitAll(visitor: SymbolVisitor, data: D): List { - return allSymbols.map { it.accept(visitor, data) } -} - -/** - * Extension function to traverse symbols matching a predicate. - */ -inline fun Scope.forEachSymbol(action: (Symbol) -> Unit) { - allSymbols.forEach(action) -} - -/** - * Extension to find symbols of a specific type. - */ -inline fun Scope.symbolsOfType(): List { - return allSymbols.filterIsInstance() -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Visibility.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Visibility.kt deleted file mode 100644 index 03751b8ae0..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/symbol/Visibility.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.symbol - -/** - * Kotlin visibility modifiers. - * - * Visibility controls which code can access a declaration. Kotlin has four visibility levels: - * - * - [PUBLIC]: Visible everywhere (default for most declarations) - * - [PRIVATE]: Visible only within the containing declaration - * - [PROTECTED]: Visible within the class and its subclasses - * - [INTERNAL]: Visible within the same module - * - * ## Default Visibility - * - * The default visibility depends on the declaration context: - * - Top-level declarations: [PUBLIC] - * - Class members: [PUBLIC] - * - Local declarations: No visibility modifier applies - * - * ## Visibility Rules - * - * - [PROTECTED] is only valid for class members, not top-level declarations - * - [PRIVATE] at top-level means visible within the file - * - [PRIVATE] in a class means visible within the class - * - [INTERNAL] is visible within the same Kotlin module (compilation unit) - * - * @property keyword The Kotlin keyword for this visibility - * @property ordinal Lower ordinal = more restrictive - */ -enum class Visibility(val keyword: String) { - /** - * Visible only within the containing declaration. - * - * For top-level declarations: visible within the same file. - * For class members: visible within the class body. - */ - PRIVATE("private"), - - /** - * Visible within the class and its subclasses. - * - * Only valid for class members. Cannot be applied to top-level declarations. - */ - PROTECTED("protected"), - - /** - * Visible within the same module. - * - * A module is a set of Kotlin files compiled together: - * - An IntelliJ IDEA module - * - A Maven project - * - A Gradle source set - */ - INTERNAL("internal"), - - /** - * Visible everywhere. - * - * This is the default visibility for most declarations. - */ - PUBLIC("public"); - - /** - * Checks if this visibility is at least as permissive as another. - * - * PUBLIC >= INTERNAL >= PROTECTED >= PRIVATE - */ - fun isAtLeast(other: Visibility): Boolean = this.ordinal >= other.ordinal - - /** - * Checks if this visibility is more restrictive than another. - */ - fun isMoreRestrictiveThan(other: Visibility): Boolean = this.ordinal < other.ordinal - - companion object { - /** - * The default visibility for declarations without an explicit modifier. - */ - val DEFAULT: Visibility = PUBLIC - - /** - * Parses a visibility from its keyword. - * - * @param keyword The visibility keyword (e.g., "private", "public") - * @return The corresponding [Visibility], or null if not recognized - */ - fun fromKeyword(keyword: String): Visibility? { - return entries.find { it.keyword == keyword } - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/KotlinType.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/KotlinType.kt deleted file mode 100644 index 5b9f62deb9..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/KotlinType.kt +++ /dev/null @@ -1,564 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.types - -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ClassSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeParameterSymbol - -/** - * Base class for all Kotlin types. - * - * Kotlin's type system includes: - * - Primitive types: Int, Long, Float, Double, Boolean, Char, Byte, Short - * - Class types: String, List, custom classes - * - Function types: (Int) -> String, suspend () -> Unit - * - Nullable types: String?, Int? - * - Type parameters: T, in T, out T - * - Special types: Unit, Nothing, Any, Any? - * - * ## Nullability - * - * All types support nullability through [nullable] and [nonNullable] methods. - * The [isNullable] property indicates whether null is a valid value. - * - * ## Type Parameters - * - * Generic types use [TypeArgument] for type arguments. Type parameters - * can be substituted using [substitute]. - * - * ## Example - * - * ```kotlin - * val stringType = ClassType.of("kotlin.String") - * val nullableString = stringType.nullable() - * val listOfStrings = ClassType.of("kotlin.collections.List", stringType) - * ``` - * - * @see ClassType - * @see FunctionType - * @see TypeParameter - */ -sealed class KotlinType { - - /** - * Whether this type is nullable (allows null values). - */ - abstract val isNullable: Boolean - - /** - * Whether this is a type parameter (unbound generic). - */ - open val isTypeParameter: Boolean get() = false - - /** - * Whether this is the Nothing type. - */ - open val isNothing: Boolean get() = false - - /** - * Whether this is the Unit type. - */ - open val isUnit: Boolean get() = false - - /** - * Whether this is the Any type. - */ - open val isAny: Boolean get() = false - - /** - * Whether this type contains errors (unresolved references). - */ - open val hasError: Boolean get() = false - - /** - * Returns a nullable version of this type. - */ - abstract fun nullable(): KotlinType - - /** - * Returns a non-nullable version of this type. - */ - abstract fun nonNullable(): KotlinType - - /** - * Substitutes type parameters with concrete types. - * - * @param substitution Map from type parameters to their replacements - * @return The type with substitutions applied - */ - abstract fun substitute(substitution: TypeSubstitution): KotlinType - - /** - * Renders this type as a readable string. - * - * @param qualified Whether to use fully qualified names - * @return String representation of the type - */ - abstract fun render(qualified: Boolean = false): String - - /** - * Returns all type parameters used in this type. - */ - abstract fun typeParameters(): Set - - /** - * Checks if this type mentions a specific type parameter. - */ - fun mentionsTypeParameter(param: TypeParameter): Boolean { - return param in typeParameters() - } - - override fun toString(): String = render() -} - -/** - * A class or interface type, optionally with type arguments. - * - * Represents types like: - * - `String` - * - `List` - * - `Map>` - * - `Comparable` - * - * @property fqName Fully qualified name (e.g., "kotlin.String") - * @property typeArguments Type arguments for generic types - * @property symbol The resolved class symbol (if available) - */ -data class ClassType( - val fqName: String, - val typeArguments: List = emptyList(), - override val isNullable: Boolean = false, - val symbol: ClassSymbol? = null -) : KotlinType() { - - /** - * The simple name (without package). - */ - val simpleName: String get() = fqName.substringAfterLast('.') - - /** - * Whether this is a generic type. - */ - val isGeneric: Boolean get() = typeArguments.isNotEmpty() - - /** - * The number of type arguments. - */ - val arity: Int get() = typeArguments.size - - override val isAny: Boolean get() = fqName == "kotlin.Any" - override val isUnit: Boolean get() = fqName == "kotlin.Unit" - override val isNothing: Boolean get() = fqName == "kotlin.Nothing" - - override fun nullable(): ClassType = copy(isNullable = true) - override fun nonNullable(): ClassType = copy(isNullable = false) - - override fun substitute(substitution: TypeSubstitution): KotlinType { - if (typeArguments.isEmpty()) return this - - val newArgs = typeArguments.map { it.substitute(substitution) } - return if (newArgs == typeArguments) this else copy(typeArguments = newArgs) - } - - override fun render(qualified: Boolean): String = buildString { - append(if (qualified) fqName else simpleName) - if (typeArguments.isNotEmpty()) { - append('<') - typeArguments.joinTo(this) { it.render(qualified) } - append('>') - } - if (isNullable) append('?') - } - - override fun typeParameters(): Set { - return typeArguments.flatMap { it.type?.typeParameters() ?: emptySet() }.toSet() - } - - /** - * Creates a type with the given type arguments. - */ - fun withArguments(vararg args: KotlinType): ClassType { - return copy(typeArguments = args.map { TypeArgument.invariant(it) }) - } - - companion object { - fun of(fqName: String, nullable: Boolean = false): ClassType { - return ClassType(fqName, isNullable = nullable) - } - - fun of(fqName: String, vararg typeArgs: KotlinType): ClassType { - return ClassType(fqName, typeArgs.map { TypeArgument.invariant(it) }) - } - - val ANY = ClassType("kotlin.Any") - val ANY_NULLABLE = ClassType("kotlin.Any", isNullable = true) - val UNIT = ClassType("kotlin.Unit") - val NOTHING = ClassType("kotlin.Nothing") - val STRING = ClassType("kotlin.String") - val CHAR_SEQUENCE = ClassType("kotlin.CharSequence") - val COMPARABLE = ClassType("kotlin.Comparable") - val ITERABLE = ClassType("kotlin.collections.Iterable") - val COLLECTION = ClassType("kotlin.collections.Collection") - val LIST = ClassType("kotlin.collections.List") - val SET = ClassType("kotlin.collections.Set") - val MAP = ClassType("kotlin.collections.Map") - val SEQUENCE = ClassType("kotlin.sequences.Sequence") - val ARRAY = ClassType("kotlin.Array") - val THROWABLE = ClassType("kotlin.Throwable") - val EXCEPTION = ClassType("kotlin.Exception") - } -} - -/** - * A primitive type. - * - * Kotlin's primitive types are: - * - Numeric: Byte, Short, Int, Long, Float, Double - * - Boolean: Boolean - * - Character: Char - * - * On the JVM, these map to Java primitive types when not nullable. - */ -data class PrimitiveType( - val kind: PrimitiveKind, - override val isNullable: Boolean = false -) : KotlinType() { - - val fqName: String get() = "kotlin.${kind.name.lowercase().replaceFirstChar { it.uppercase() }}" - - override fun nullable(): PrimitiveType = copy(isNullable = true) - override fun nonNullable(): PrimitiveType = copy(isNullable = false) - - override fun substitute(substitution: TypeSubstitution): KotlinType = this - - override fun render(qualified: Boolean): String = buildString { - append(kind.name.lowercase().replaceFirstChar { it.uppercase() }) - if (isNullable) append('?') - } - - override fun typeParameters(): Set = emptySet() - - companion object { - val BYTE = PrimitiveType(PrimitiveKind.BYTE) - val SHORT = PrimitiveType(PrimitiveKind.SHORT) - val INT = PrimitiveType(PrimitiveKind.INT) - val LONG = PrimitiveType(PrimitiveKind.LONG) - val FLOAT = PrimitiveType(PrimitiveKind.FLOAT) - val DOUBLE = PrimitiveType(PrimitiveKind.DOUBLE) - val BOOLEAN = PrimitiveType(PrimitiveKind.BOOLEAN) - val CHAR = PrimitiveType(PrimitiveKind.CHAR) - - fun fromName(name: String): PrimitiveType? { - val kind = PrimitiveKind.entries.find { - it.name.equals(name, ignoreCase = true) - } ?: return null - return PrimitiveType(kind) - } - } -} - -/** - * Kind of primitive type. - */ -enum class PrimitiveKind { - BYTE, - SHORT, - INT, - LONG, - FLOAT, - DOUBLE, - BOOLEAN, - CHAR; - - val isNumeric: Boolean get() = this != BOOLEAN && this != CHAR - val isIntegral: Boolean get() = this in listOf(BYTE, SHORT, INT, LONG) - val isFloatingPoint: Boolean get() = this == FLOAT || this == DOUBLE - - val bitWidth: Int get() = when (this) { - BYTE -> 8 - SHORT, CHAR -> 16 - INT, FLOAT -> 32 - LONG, DOUBLE -> 64 - BOOLEAN -> 1 - } -} - -/** - * A function type. - * - * Represents types like: - * - `() -> Unit` - * - `(Int) -> String` - * - `(Int, String) -> Boolean` - * - `suspend () -> Unit` - * - `Int.() -> String` (extension function type) - * - * @property parameterTypes Types of function parameters - * @property returnType Return type of the function - * @property receiverType Receiver type for extension function types - * @property isSuspend Whether this is a suspend function type - */ -data class FunctionType( - val parameterTypes: List, - val returnType: KotlinType, - val receiverType: KotlinType? = null, - val isSuspend: Boolean = false, - override val isNullable: Boolean = false -) : KotlinType() { - - /** - * Number of parameters (not including receiver). - */ - val arity: Int get() = parameterTypes.size - - /** - * Whether this is an extension function type. - */ - val isExtension: Boolean get() = receiverType != null - - override fun nullable(): FunctionType = copy(isNullable = true) - override fun nonNullable(): FunctionType = copy(isNullable = false) - - override fun substitute(substitution: TypeSubstitution): KotlinType { - val newParams = parameterTypes.map { it.substitute(substitution) } - val newReturn = returnType.substitute(substitution) - val newReceiver = receiverType?.substitute(substitution) - - return if (newParams == parameterTypes && newReturn == returnType && newReceiver == receiverType) { - this - } else { - copy( - parameterTypes = newParams, - returnType = newReturn, - receiverType = newReceiver - ) - } - } - - override fun render(qualified: Boolean): String = buildString { - if (isSuspend) append("suspend ") - if (receiverType != null) { - append(receiverType.render(qualified)) - append('.') - } - append('(') - parameterTypes.joinTo(this) { it.render(qualified) } - append(") -> ") - append(returnType.render(qualified)) - if (isNullable) { - insert(0, '(') - append(")?") - } - } - - override fun typeParameters(): Set { - val params = mutableSetOf() - parameterTypes.forEach { params.addAll(it.typeParameters()) } - params.addAll(returnType.typeParameters()) - receiverType?.let { params.addAll(it.typeParameters()) } - return params - } - - companion object { - fun of(vararg paramTypes: KotlinType, returnType: KotlinType): FunctionType { - return FunctionType(paramTypes.toList(), returnType) - } - - fun suspend(vararg paramTypes: KotlinType, returnType: KotlinType): FunctionType { - return FunctionType(paramTypes.toList(), returnType, isSuspend = true) - } - } -} - -/** - * A type parameter (generic type variable). - * - * Represents unbound type variables like `T` in `class Box`. - * - * @property name The name of the type parameter - * @property bounds Upper bounds (default is Any?) - * @property variance Variance modifier (in/out/invariant) - * @property symbol The resolved symbol (if available) - */ -data class TypeParameter( - val name: String, - val bounds: List = emptyList(), - val variance: TypeVariance = TypeVariance.INVARIANT, - override val isNullable: Boolean = false, - val symbol: TypeParameterSymbol? = null -) : KotlinType() { - - override val isTypeParameter: Boolean get() = true - - /** - * The effective upper bound (first bound or Any?). - */ - val effectiveBound: KotlinType - get() = bounds.firstOrNull() ?: ClassType.ANY_NULLABLE - - /** - * Whether this type parameter has explicit bounds. - */ - val hasBounds: Boolean get() = bounds.isNotEmpty() - - override fun nullable(): TypeParameter = copy(isNullable = true) - override fun nonNullable(): TypeParameter = copy(isNullable = false) - - override fun substitute(substitution: TypeSubstitution): KotlinType { - val replacement = substitution[this] - return if (replacement != null) { - if (isNullable) replacement.nullable() else replacement - } else { - this - } - } - - override fun render(qualified: Boolean): String = buildString { - when (variance) { - TypeVariance.IN -> append("in ") - TypeVariance.OUT -> append("out ") - TypeVariance.INVARIANT -> {} - } - append(name) - if (isNullable) append('?') - } - - override fun typeParameters(): Set = setOf(this) - - companion object { - fun of(name: String): TypeParameter = TypeParameter(name) - - fun withBound(name: String, bound: KotlinType): TypeParameter { - return TypeParameter(name, listOf(bound)) - } - } -} - -/** - * Variance for type parameters and arguments. - */ -enum class TypeVariance { - INVARIANT, - IN, - OUT; - - companion object { - fun fromKeyword(keyword: String): TypeVariance = when (keyword) { - "in" -> IN - "out" -> OUT - else -> INVARIANT - } - } -} - -/** - * A type argument in a generic type. - * - * Can be: - * - An actual type: `List` has type argument `String` - * - A projection: `List`, `List` - * - A star projection: `List<*>` - * - * @property type The actual type (null for star projection) - * @property variance Variance for projections - */ -data class TypeArgument( - val type: KotlinType?, - val variance: TypeVariance = TypeVariance.INVARIANT -) { - /** - * Whether this is a star projection (*). - */ - val isStarProjection: Boolean get() = type == null - - /** - * Whether this is a projection (in T, out T). - */ - val isProjection: Boolean get() = variance != TypeVariance.INVARIANT - - fun substitute(substitution: TypeSubstitution): TypeArgument { - if (type == null) return this - val newType = type.substitute(substitution) - return if (newType === type) this else copy(type = newType) - } - - fun render(qualified: Boolean = false): String = when { - isStarProjection -> "*" - variance == TypeVariance.IN -> "in ${type!!.render(qualified)}" - variance == TypeVariance.OUT -> "out ${type!!.render(qualified)}" - else -> type!!.render(qualified) - } - - override fun toString(): String = render() - - companion object { - val STAR = TypeArgument(null) - - fun invariant(type: KotlinType) = TypeArgument(type, TypeVariance.INVARIANT) - fun covariant(type: KotlinType) = TypeArgument(type, TypeVariance.OUT) - fun contravariant(type: KotlinType) = TypeArgument(type, TypeVariance.IN) - } -} - -/** - * An error/unknown type for unresolved references. - */ -data class ErrorType( - val message: String = "Unresolved type", - override val isNullable: Boolean = false -) : KotlinType() { - - override val hasError: Boolean get() = true - - override fun nullable(): ErrorType = copy(isNullable = true) - override fun nonNullable(): ErrorType = copy(isNullable = false) - - override fun substitute(substitution: TypeSubstitution): KotlinType = this - - override fun render(qualified: Boolean): String = "" - - override fun typeParameters(): Set = emptySet() - - companion object { - val UNRESOLVED = ErrorType("Unresolved type") - - fun unresolved(name: String) = ErrorType("Unresolved type: $name") - } -} - -/** - * Map from type parameters to their replacement types. - */ -class TypeSubstitution private constructor( - private val map: Map -) { - operator fun get(param: TypeParameter): KotlinType? = map[param] - - operator fun contains(param: TypeParameter): Boolean = param in map - - val isEmpty: Boolean get() = map.isEmpty() - - val entries: Set> get() = map.entries - - fun with(param: TypeParameter, type: KotlinType): TypeSubstitution { - return TypeSubstitution(map + (param to type)) - } - - fun compose(other: TypeSubstitution): TypeSubstitution { - val newMap = map.mapValues { (_, type) -> type.substitute(other) } + other.map - return TypeSubstitution(newMap) - } - - companion object { - val EMPTY = TypeSubstitution(emptyMap()) - - fun of(vararg pairs: Pair): TypeSubstitution { - return TypeSubstitution(pairs.toMap()) - } - - fun from(params: List, args: List): TypeSubstitution { - require(params.size == args.size) { - "Parameter count ${params.size} doesn't match argument count ${args.size}" - } - return TypeSubstitution(params.zip(args).toMap()) - } - } -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/TypeChecker.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/TypeChecker.kt deleted file mode 100644 index 0b30508699..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/TypeChecker.kt +++ /dev/null @@ -1,433 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.types - -/** - * Checks subtype relationships and type compatibility. - * - * Implements Kotlin's subtyping rules: - * - Nothing is a subtype of all types - * - All types are subtypes of Any? - * - T <: T? (non-nullable is subtype of nullable) - * - Variance affects generic subtyping - * - * ## Subtyping Rules - * - * ``` - * Nothing <: T <: Any? - * T <: T? - * List <: List (covariance) - * (Animal) -> Unit <: (Cat) -> Unit (contravariance in params) - * () -> Cat <: () -> Animal (covariance in return) - * ``` - * - * @see KotlinType - * @see TypeHierarchy - */ -class TypeChecker( - private val hierarchy: TypeHierarchy = TypeHierarchy.DEFAULT -) { - /** - * Checks if [subtype] is a subtype of [supertype]. - * - * @param subtype The potential subtype - * @param supertype The potential supertype - * @return true if subtype <: supertype - */ - fun isSubtypeOf(subtype: KotlinType, supertype: KotlinType): Boolean { - if (subtype == supertype) return true - - if (subtype.hasError || supertype.hasError) return true - - if (subtype.isNothing) return true - - if (supertype is ClassType && supertype.fqName == "kotlin.Any") { - return supertype.isNullable || !subtype.isNullable - } - - if (!supertype.isNullable && subtype.isNullable) { - return false - } - - return when { - subtype is ClassType && supertype is ClassType -> - isClassSubtype(subtype, supertype) - - subtype is PrimitiveType && supertype is PrimitiveType -> - isPrimitiveSubtype(subtype, supertype) - - subtype is PrimitiveType && supertype is ClassType -> - isPrimitiveToClassSubtype(subtype, supertype) - - subtype is FunctionType && supertype is FunctionType -> - isFunctionSubtype(subtype, supertype) - - subtype is TypeParameter && supertype is TypeParameter -> - isTypeParameterSubtype(subtype, supertype) - - subtype is TypeParameter -> - isTypeParameterSubtypeOf(subtype, supertype) - - supertype is TypeParameter -> - isSubtypeOfTypeParameter(subtype, supertype) - - else -> false - } - } - - /** - * Checks if two types are equivalent (mutual subtypes). - */ - fun areEquivalent(type1: KotlinType, type2: KotlinType): Boolean { - return isSubtypeOf(type1, type2) && isSubtypeOf(type2, type1) - } - - /** - * Checks if a type is assignable to a target type. - * - * This is slightly more permissive than subtyping for practical use. - */ - fun isAssignableTo(source: KotlinType, target: KotlinType): Boolean { - return isSubtypeOf(source, target) - } - - /** - * Finds the common supertype of two types. - * - * Used for inferring result types of if/when expressions. - */ - fun commonSupertype(type1: KotlinType, type2: KotlinType): KotlinType { - if (isSubtypeOf(type1, type2)) return type2 - if (isSubtypeOf(type2, type1)) return type1 - - if (type1.isNothing) return type2 - if (type2.isNothing) return type1 - - val nullable = type1.isNullable || type2.isNullable - - return when { - type1 is ClassType && type2 is ClassType -> { - val common = findCommonSuperclass(type1, type2) - if (nullable) common.nullable() else common - } - type1 is PrimitiveType && type2 is PrimitiveType -> { - val common = commonPrimitiveSupertype(type1, type2) - if (nullable) common.nullable() else common - } - else -> { - if (nullable) ClassType.ANY_NULLABLE else ClassType.ANY - } - } - } - - /** - * Finds the common supertype of multiple types. - */ - fun commonSupertype(types: List): KotlinType { - if (types.isEmpty()) return ClassType.NOTHING - return types.reduce { acc, type -> commonSupertype(acc, type) } - } - - private fun isClassSubtype(subtype: ClassType, supertype: ClassType): Boolean { - if (subtype.fqName == supertype.fqName) { - return checkTypeArguments(subtype.typeArguments, supertype.typeArguments) - } - - val supertypes = hierarchy.getSupertypes(subtype.fqName) - for (st in supertypes) { - if (st.fqName == supertype.fqName) { - return checkTypeArguments(st.typeArguments, supertype.typeArguments) - } - if (isClassSubtype(st, supertype)) { - return true - } - } - - return false - } - - private fun checkTypeArguments( - subArgs: List, - superArgs: List - ): Boolean { - if (subArgs.size != superArgs.size) return false - - return subArgs.zip(superArgs).all { (subArg, superArg) -> - checkTypeArgument(subArg, superArg) - } - } - - private fun checkTypeArgument(subArg: TypeArgument, superArg: TypeArgument): Boolean { - if (superArg.isStarProjection) return true - if (subArg.isStarProjection) return false - - val subType = subArg.type!! - val superType = superArg.type!! - - return when (superArg.variance) { - TypeVariance.INVARIANT -> when (subArg.variance) { - TypeVariance.INVARIANT -> areEquivalent(subType, superType) - else -> false - } - TypeVariance.OUT -> isSubtypeOf(subType, superType) - TypeVariance.IN -> isSubtypeOf(superType, subType) - } - } - - private fun isPrimitiveSubtype(subtype: PrimitiveType, supertype: PrimitiveType): Boolean { - if (subtype.kind == supertype.kind) return true - - return when (subtype.kind) { - PrimitiveKind.BYTE -> supertype.kind in listOf( - PrimitiveKind.SHORT, PrimitiveKind.INT, PrimitiveKind.LONG, - PrimitiveKind.FLOAT, PrimitiveKind.DOUBLE - ) - PrimitiveKind.SHORT -> supertype.kind in listOf( - PrimitiveKind.INT, PrimitiveKind.LONG, - PrimitiveKind.FLOAT, PrimitiveKind.DOUBLE - ) - PrimitiveKind.INT -> supertype.kind in listOf( - PrimitiveKind.LONG, PrimitiveKind.FLOAT, PrimitiveKind.DOUBLE - ) - PrimitiveKind.LONG -> supertype.kind in listOf( - PrimitiveKind.FLOAT, PrimitiveKind.DOUBLE - ) - PrimitiveKind.FLOAT -> supertype.kind == PrimitiveKind.DOUBLE - else -> false - } - } - - private fun isPrimitiveToClassSubtype(subtype: PrimitiveType, supertype: ClassType): Boolean { - if (supertype.fqName == "kotlin.Any") return true - - val boxedFqName = when (subtype.kind) { - PrimitiveKind.INT -> "kotlin.Int" - PrimitiveKind.LONG -> "kotlin.Long" - PrimitiveKind.FLOAT -> "kotlin.Float" - PrimitiveKind.DOUBLE -> "kotlin.Double" - PrimitiveKind.BOOLEAN -> "kotlin.Boolean" - PrimitiveKind.CHAR -> "kotlin.Char" - PrimitiveKind.BYTE -> "kotlin.Byte" - PrimitiveKind.SHORT -> "kotlin.Short" - } - - if (supertype.fqName == boxedFqName) return true - - val boxedType = ClassType(boxedFqName, isNullable = subtype.isNullable) - return isClassSubtype(boxedType, supertype) - } - - private fun isFunctionSubtype(subtype: FunctionType, supertype: FunctionType): Boolean { - if (subtype.arity != supertype.arity) return false - - if (subtype.isSuspend && !supertype.isSuspend) return false - - if (!isSubtypeOf(subtype.returnType, supertype.returnType)) return false - - for ((subParam, superParam) in subtype.parameterTypes.zip(supertype.parameterTypes)) { - if (!isSubtypeOf(superParam, subParam)) return false - } - - if (supertype.receiverType != null) { - if (subtype.receiverType == null) return false - if (!isSubtypeOf(supertype.receiverType, subtype.receiverType)) return false - } - - return true - } - - private fun isTypeParameterSubtype( - subtype: TypeParameter, - supertype: TypeParameter - ): Boolean { - if (subtype.name == supertype.name && subtype.symbol == supertype.symbol) { - return true - } - - for (bound in subtype.bounds) { - if (bound is TypeParameter && isTypeParameterSubtype(bound, supertype)) { - return true - } - } - - return false - } - - private fun isTypeParameterSubtypeOf( - subtype: TypeParameter, - supertype: KotlinType - ): Boolean { - for (bound in subtype.bounds) { - if (isSubtypeOf(bound, supertype)) { - return true - } - } - - return isSubtypeOf(ClassType.ANY_NULLABLE, supertype) - } - - private fun isSubtypeOfTypeParameter( - subtype: KotlinType, - supertype: TypeParameter - ): Boolean { - if (subtype.isNothing) return true - - for (bound in supertype.bounds) { - if (!isSubtypeOf(subtype, bound)) { - return false - } - } - return true - } - - private fun findCommonSuperclass(type1: ClassType, type2: ClassType): ClassType { - val supertypes1 = collectAllSupertypes(type1) - val supertypes2 = collectAllSupertypes(type2) - - for (st in supertypes1) { - if (st.fqName in supertypes2.map { it.fqName }) { - return st - } - } - - return ClassType.ANY - } - - private fun collectAllSupertypes(type: ClassType): List { - val result = mutableListOf(type) - val toProcess = ArrayDeque() - toProcess.add(type) - - while (toProcess.isNotEmpty()) { - val current = toProcess.removeFirst() - val supertypes = hierarchy.getSupertypes(current.fqName) - for (st in supertypes) { - if (st !in result) { - result.add(st) - toProcess.add(st) - } - } - } - - return result - } - - private fun commonPrimitiveSupertype( - type1: PrimitiveType, - type2: PrimitiveType - ): PrimitiveType { - if (type1.kind == type2.kind) return type1 - - if (type1.kind.isNumeric && type2.kind.isNumeric) { - val wider = if (type1.kind.bitWidth >= type2.kind.bitWidth) type1.kind else type2.kind - val isFloating = type1.kind.isFloatingPoint || type2.kind.isFloatingPoint - - return when { - isFloating && wider.bitWidth <= 32 -> PrimitiveType.FLOAT - isFloating -> PrimitiveType.DOUBLE - else -> PrimitiveType(wider) - } - } - - return PrimitiveType.INT - } - - companion object { - val DEFAULT = TypeChecker() - } -} - -/** - * Type hierarchy for determining supertypes of classes. - */ -class TypeHierarchy { - private val supertypes = mutableMapOf>() - - init { - registerBuiltins() - } - - fun getSupertypes(fqName: String): List { - return supertypes[fqName] ?: emptyList() - } - - fun registerSupertypes(fqName: String, types: List) { - supertypes[fqName] = types - } - - private fun registerBuiltins() { - supertypes["kotlin.String"] = listOf( - ClassType.COMPARABLE.withArguments(ClassType.STRING), - ClassType.CHAR_SEQUENCE - ) - - supertypes["kotlin.Int"] = listOf( - ClassType.COMPARABLE.withArguments(PrimitiveType.INT.toClassType()), - ClassType("kotlin.Number") - ) - supertypes["kotlin.Long"] = listOf( - ClassType.COMPARABLE.withArguments(PrimitiveType.LONG.toClassType()), - ClassType("kotlin.Number") - ) - supertypes["kotlin.Float"] = listOf( - ClassType.COMPARABLE.withArguments(PrimitiveType.FLOAT.toClassType()), - ClassType("kotlin.Number") - ) - supertypes["kotlin.Double"] = listOf( - ClassType.COMPARABLE.withArguments(PrimitiveType.DOUBLE.toClassType()), - ClassType("kotlin.Number") - ) - supertypes["kotlin.Short"] = listOf( - ClassType.COMPARABLE.withArguments(PrimitiveType.SHORT.toClassType()), - ClassType("kotlin.Number") - ) - supertypes["kotlin.Byte"] = listOf( - ClassType.COMPARABLE.withArguments(PrimitiveType.BYTE.toClassType()), - ClassType("kotlin.Number") - ) - supertypes["kotlin.Char"] = listOf( - ClassType.COMPARABLE.withArguments(PrimitiveType.CHAR.toClassType()) - ) - supertypes["kotlin.Boolean"] = listOf( - ClassType.COMPARABLE.withArguments(PrimitiveType.BOOLEAN.toClassType()) - ) - - supertypes["kotlin.Number"] = listOf(ClassType.ANY) - - supertypes["kotlin.collections.List"] = listOf(ClassType.COLLECTION) - supertypes["kotlin.collections.Set"] = listOf(ClassType.COLLECTION) - supertypes["kotlin.collections.Collection"] = listOf(ClassType.ITERABLE) - supertypes["kotlin.collections.Iterable"] = listOf(ClassType.ANY) - supertypes["kotlin.collections.Map"] = listOf(ClassType.ANY) - - supertypes["kotlin.collections.MutableList"] = listOf( - ClassType.LIST, - ClassType("kotlin.collections.MutableCollection") - ) - supertypes["kotlin.collections.MutableSet"] = listOf( - ClassType.SET, - ClassType("kotlin.collections.MutableCollection") - ) - supertypes["kotlin.collections.MutableCollection"] = listOf( - ClassType.COLLECTION, - ClassType("kotlin.collections.MutableIterable") - ) - supertypes["kotlin.collections.MutableIterable"] = listOf(ClassType.ITERABLE) - - supertypes["kotlin.Throwable"] = listOf(ClassType.ANY) - supertypes["kotlin.Exception"] = listOf(ClassType.THROWABLE) - supertypes["kotlin.RuntimeException"] = listOf(ClassType.EXCEPTION) - supertypes["kotlin.IllegalArgumentException"] = listOf(ClassType("kotlin.RuntimeException")) - supertypes["kotlin.IllegalStateException"] = listOf(ClassType("kotlin.RuntimeException")) - supertypes["kotlin.NullPointerException"] = listOf(ClassType("kotlin.RuntimeException")) - } - - companion object { - val DEFAULT = TypeHierarchy() - } -} - -/** - * Extension to convert PrimitiveType to its boxed ClassType. - */ -fun PrimitiveType.toClassType(): ClassType { - return ClassType(fqName, isNullable = isNullable) -} diff --git a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/TypeResolver.kt b/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/TypeResolver.kt deleted file mode 100644 index e6231953d7..0000000000 --- a/lsp/kotlin-core/src/main/java/org/appdevforall/codeonthego/lsp/kotlin/types/TypeResolver.kt +++ /dev/null @@ -1,389 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.types - -import org.appdevforall.codeonthego.lsp.kotlin.index.IndexedSymbol -import org.appdevforall.codeonthego.lsp.kotlin.index.ProjectIndex -import org.appdevforall.codeonthego.lsp.kotlin.symbol.ImportInfo -import org.appdevforall.codeonthego.lsp.kotlin.symbol.SymbolTable -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeParameterSymbol -import org.appdevforall.codeonthego.lsp.kotlin.symbol.TypeReference -import org.appdevforall.codeonthego.lsp.kotlin.symbol.Scope - -/** - * Resolves type references to concrete KotlinType instances. - * - * TypeResolver handles: - * - Looking up class types by name - * - Resolving type arguments for generic types - * - Handling nullable types - * - Recognizing primitive and built-in types - * - * ## Usage - * - * ```kotlin - * val resolver = TypeResolver() - * val typeRef = TypeReference("List", listOf(TypeReference("String"))) - * val type = resolver.resolve(typeRef, scope) - * // type is ClassType("kotlin.collections.List", [ClassType("kotlin.String")]) - * ``` - */ -class TypeResolver( - private val hierarchy: TypeHierarchy = TypeHierarchy.DEFAULT -) { - private val primitiveNames = mapOf( - "Int" to PrimitiveType.INT, - "Long" to PrimitiveType.LONG, - "Float" to PrimitiveType.FLOAT, - "Double" to PrimitiveType.DOUBLE, - "Boolean" to PrimitiveType.BOOLEAN, - "Char" to PrimitiveType.CHAR, - "Byte" to PrimitiveType.BYTE, - "Short" to PrimitiveType.SHORT - ) - - private val builtinTypes = mapOf( - "Any" to ClassType.ANY, - "Unit" to ClassType.UNIT, - "Nothing" to ClassType.NOTHING, - "String" to ClassType.STRING, - "CharSequence" to ClassType.CHAR_SEQUENCE, - "Number" to ClassType("kotlin.Number"), - "Comparable" to ClassType.COMPARABLE, - "Throwable" to ClassType.THROWABLE, - "Exception" to ClassType.EXCEPTION, - - "Array" to ClassType.ARRAY, - "IntArray" to ClassType("kotlin.IntArray"), - "LongArray" to ClassType("kotlin.LongArray"), - "FloatArray" to ClassType("kotlin.FloatArray"), - "DoubleArray" to ClassType("kotlin.DoubleArray"), - "BooleanArray" to ClassType("kotlin.BooleanArray"), - "CharArray" to ClassType("kotlin.CharArray"), - "ByteArray" to ClassType("kotlin.ByteArray"), - "ShortArray" to ClassType("kotlin.ShortArray"), - - "List" to ClassType.LIST, - "MutableList" to ClassType("kotlin.collections.MutableList"), - "Set" to ClassType.SET, - "MutableSet" to ClassType("kotlin.collections.MutableSet"), - "Map" to ClassType.MAP, - "MutableMap" to ClassType("kotlin.collections.MutableMap"), - "Collection" to ClassType.COLLECTION, - "MutableCollection" to ClassType("kotlin.collections.MutableCollection"), - "Iterable" to ClassType.ITERABLE, - "MutableIterable" to ClassType("kotlin.collections.MutableIterable"), - "Sequence" to ClassType.SEQUENCE, - - "Pair" to ClassType("kotlin.Pair"), - "Triple" to ClassType("kotlin.Triple"), - "Lazy" to ClassType("kotlin.Lazy"), - "Result" to ClassType("kotlin.Result") - ) - - /** - * Resolves a TypeReference to a KotlinType. - * - * @param ref The type reference from parsing - * @param scope The scope for resolving type parameters - * @param imports Optional list of imports for resolving non-qualified type names - * @param indexLookup Optional function to validate FQ names against the index - * @return The resolved KotlinType, or ErrorType if unresolved - */ - fun resolve( - ref: TypeReference, - scope: Scope? = null, - imports: List = emptyList(), - indexLookup: ((String) -> IndexedSymbol?)? = null - ): KotlinType { - if (ref.isFunctionType && ref.functionTypeInfo != null) { - val info = ref.functionTypeInfo - val functionType = FunctionType( - parameterTypes = info.parameterTypes.map { resolve(it, scope, imports, indexLookup) }, - returnType = resolve(info.returnType, scope, imports, indexLookup), - receiverType = info.receiverType?.let { resolve(it, scope, imports, indexLookup) } - ) - return if (ref.isNullable) functionType.nullable() else functionType - } - - val baseType = resolveBaseName(ref.name, scope, imports, indexLookup) - - val withArgs = if (ref.typeArguments.isNotEmpty() && baseType is ClassType) { - val resolvedArgs = ref.typeArguments.map { argRef -> - TypeArgument.invariant(resolve(argRef, scope, imports, indexLookup)) - } - baseType.copy(typeArguments = resolvedArgs) - } else { - baseType - } - - return if (ref.isNullable) withArgs.nullable() else withArgs - } - - /** - * Resolves a simple type name. - * - * @param name The type name to resolve - * @param scope The scope for resolving type parameters - * @param imports List of imports for resolving non-qualified type names - * @param indexLookup Optional function to validate FQ names against the index - */ - fun resolveBaseName( - name: String, - scope: Scope? = null, - imports: List = emptyList(), - indexLookup: ((String) -> IndexedSymbol?)? = null - ): KotlinType { - val simpleName = name.substringAfterLast('.') - - primitiveNames[simpleName]?.let { return it } - - builtinTypes[simpleName]?.let { return it } - - if (name.startsWith("kotlin.")) { - return ClassType(name) - } - - if (scope != null) { - val typeParam = scope.resolve(simpleName) - .filterIsInstance() - .firstOrNull() - - if (typeParam != null) { - return TypeParameter( - name = typeParam.name, - bounds = typeParam.bounds.mapNotNull { resolve(it, scope) as? KotlinType }, - symbol = typeParam - ) - } - } - - if ('.' in name) { - if (indexLookup != null) { - val symbol = indexLookup(name) - if (symbol != null) return ClassType(name) - return ErrorType.unresolved(name) - } - return ClassType(name) - } - - for (import in imports) { - if (import.isStar) continue - val importedName = import.alias ?: import.fqName.substringAfterLast('.') - if (importedName == simpleName) { - if (indexLookup != null) { - val symbol = indexLookup(import.fqName) - if (symbol != null) return ClassType(import.fqName) - continue - } - return ClassType(import.fqName) - } - } - - if (indexLookup != null) { - for (pkg in KOTLIN_AUTO_IMPORTS) { - val candidateFqn = "$pkg.$simpleName" - if (indexLookup(candidateFqn) != null) { - return ClassType(candidateFqn) - } - } - } - - for (import in imports) { - if (!import.isStar) continue - val candidateFqn = "${import.fqName}.$simpleName" - if (indexLookup != null) { - val symbol = indexLookup(candidateFqn) - if (symbol != null) return ClassType(candidateFqn) - continue - } - return ClassType(candidateFqn) - } - - if (indexLookup == null) { - return ClassType("kotlin.$simpleName") - } - - return ErrorType.unresolved(simpleName) - } - - /** - * Resolves a function type string like "(Int, String) -> Boolean". - */ - fun resolveFunctionType( - paramTypes: List, - returnType: TypeReference, - receiverType: TypeReference? = null, - isSuspend: Boolean = false, - scope: Scope? = null - ): FunctionType { - return FunctionType( - parameterTypes = paramTypes.map { resolve(it, scope) }, - returnType = resolve(returnType, scope), - receiverType = receiverType?.let { resolve(it, scope) }, - isSuspend = isSuspend - ) - } - - /** - * Creates a List type with the given element type. - */ - fun listOf(elementType: KotlinType): ClassType { - return ClassType.LIST.withArguments(elementType) - } - - /** - * Creates a Set type with the given element type. - */ - fun setOf(elementType: KotlinType): ClassType { - return ClassType.SET.withArguments(elementType) - } - - /** - * Creates a Map type with the given key and value types. - */ - fun mapOf(keyType: KotlinType, valueType: KotlinType): ClassType { - return ClassType.MAP.copy( - typeArguments = listOf( - TypeArgument.invariant(keyType), - TypeArgument.invariant(valueType) - ) - ) - } - - /** - * Creates an Array type with the given element type. - */ - fun arrayOf(elementType: KotlinType): ClassType { - return ClassType.ARRAY.withArguments(elementType) - } - - /** - * Creates a Comparable type with the given element type. - */ - fun comparableOf(elementType: KotlinType): ClassType { - return ClassType.COMPARABLE.withArguments(elementType) - } - - companion object { - val DEFAULT = TypeResolver() - - private val KOTLIN_AUTO_IMPORTS = listOf( - "kotlin", - "kotlin.collections", - "kotlin.sequences", - "kotlin.ranges", - "kotlin.text", - "kotlin.io", - "kotlin.annotation", - "kotlin.comparisons" - ) - - fun resolveSimple(name: String): KotlinType { - return DEFAULT.resolveBaseName(name, null) - } - } -} - -/** - * Factory for creating common types. - */ -object Types { - val INT = PrimitiveType.INT - val LONG = PrimitiveType.LONG - val FLOAT = PrimitiveType.FLOAT - val DOUBLE = PrimitiveType.DOUBLE - val BOOLEAN = PrimitiveType.BOOLEAN - val CHAR = PrimitiveType.CHAR - val BYTE = PrimitiveType.BYTE - val SHORT = PrimitiveType.SHORT - - val ANY = ClassType.ANY - val ANY_NULLABLE = ClassType.ANY_NULLABLE - val UNIT = ClassType.UNIT - val NOTHING = ClassType.NOTHING - val STRING = ClassType.STRING - val NUMBER = ClassType("kotlin.Number") - - fun nullable(type: KotlinType): KotlinType = type.nullable() - - fun list(elementType: KotlinType) = ClassType.LIST.withArguments(elementType) - - fun mutableList(elementType: KotlinType) = - ClassType("kotlin.collections.MutableList").withArguments(elementType) - - fun set(elementType: KotlinType) = ClassType.SET.withArguments(elementType) - - fun mutableSet(elementType: KotlinType) = - ClassType("kotlin.collections.MutableSet").withArguments(elementType) - - fun map(keyType: KotlinType, valueType: KotlinType) = - ClassType.MAP.copy(typeArguments = listOf( - TypeArgument.invariant(keyType), - TypeArgument.invariant(valueType) - )) - - fun mutableMap(keyType: KotlinType, valueType: KotlinType) = - ClassType("kotlin.collections.MutableMap").copy(typeArguments = listOf( - TypeArgument.invariant(keyType), - TypeArgument.invariant(valueType) - )) - - fun array(elementType: KotlinType) = ClassType.ARRAY.withArguments(elementType) - - fun function(vararg paramTypes: KotlinType, returnType: KotlinType) = - FunctionType(paramTypes.toList(), returnType) - - fun suspendFunction(vararg paramTypes: KotlinType, returnType: KotlinType) = - FunctionType(paramTypes.toList(), returnType, isSuspend = true) - - fun pair(first: KotlinType, second: KotlinType) = - ClassType("kotlin.Pair").copy(typeArguments = listOf( - TypeArgument.invariant(first), - TypeArgument.invariant(second) - )) - - fun triple(first: KotlinType, second: KotlinType, third: KotlinType) = - ClassType("kotlin.Triple").copy(typeArguments = listOf( - TypeArgument.invariant(first), - TypeArgument.invariant(second), - TypeArgument.invariant(third) - )) -} - -class ImportAwareTypeResolver( - private val symbolTable: SymbolTable, - private val projectIndex: ProjectIndex? = null -) { - private val delegate = TypeResolver() - - private val indexLookup: ((String) -> IndexedSymbol?)? = projectIndex?.let { idx -> - { fqName: String -> - idx.findByFqName(fqName) - ?: idx.getStdlibIndex()?.findByFqName(fqName) - ?: idx.getClasspathIndex()?.findByFqName(fqName) - } - } - - fun resolve(ref: TypeReference, scope: Scope? = null): KotlinType { - return delegate.resolve(ref, scope, symbolTable.imports, indexLookup) - } - - fun resolveBaseName(name: String, scope: Scope? = null): KotlinType { - return delegate.resolveBaseName(name, scope, symbolTable.imports, indexLookup) - } - - fun resolveFunctionType( - paramTypes: List, - returnType: TypeReference, - receiverType: TypeReference? = null, - isSuspend: Boolean = false, - scope: Scope? = null - ): FunctionType { - return delegate.resolveFunctionType(paramTypes, returnType, receiverType, isSuspend, scope) - } - - fun listOf(elementType: KotlinType) = delegate.listOf(elementType) - fun setOf(elementType: KotlinType) = delegate.setOf(elementType) - fun mapOf(keyType: KotlinType, valueType: KotlinType) = delegate.mapOf(keyType, valueType) - fun arrayOf(elementType: KotlinType) = delegate.arrayOf(elementType) - fun comparableOf(elementType: KotlinType) = delegate.comparableOf(elementType) -} \ No newline at end of file diff --git a/lsp/kotlin-stdlib-generator/build.gradle.kts b/lsp/kotlin-stdlib-generator/build.gradle.kts deleted file mode 100644 index 31d7722e40..0000000000 --- a/lsp/kotlin-stdlib-generator/build.gradle.kts +++ /dev/null @@ -1,43 +0,0 @@ -plugins { - id("org.jetbrains.kotlin.jvm") - kotlin("plugin.serialization") - application -} - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -tasks.withType().configureEach { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) - } -} - -application { - mainClass.set("org.appdevforall.codeonthego.lsp.kotlin.generator.StdlibIndexGeneratorKt") -} - -dependencies { - implementation(kotlin("reflect")) - implementation(kotlin("stdlib")) - implementation(libs.kotlinx.serialization.json) - implementation(libs.common.kotlin.coroutines.core) -} - -tasks.register("generateStdlibIndex") { - group = "generation" - description = "Generates the stdlib-index.json file for the Kotlin standard library" - - classpath = sourceSets.main.get().runtimeClasspath - mainClass.set("org.appdevforall.codeonthego.lsp.kotlin.generator.StdlibIndexGeneratorKt") - - val outputDir = layout.buildDirectory.dir("generated-resources/stdlib") - outputs.dir(outputDir) - args = listOf(outputDir.get().asFile.absolutePath) - - doFirst { - outputDir.get().asFile.mkdirs() - } -} diff --git a/lsp/kotlin-stdlib-generator/src/main/kotlin/org/appdevforall/codeonthego/lsp/kotlin/generator/StdlibIndexGenerator.kt b/lsp/kotlin-stdlib-generator/src/main/kotlin/org/appdevforall/codeonthego/lsp/kotlin/generator/StdlibIndexGenerator.kt deleted file mode 100644 index a2cc47acaa..0000000000 --- a/lsp/kotlin-stdlib-generator/src/main/kotlin/org/appdevforall/codeonthego/lsp/kotlin/generator/StdlibIndexGenerator.kt +++ /dev/null @@ -1,1284 +0,0 @@ -package org.appdevforall.codeonthego.lsp.kotlin.generator - -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import java.io.File -import kotlin.reflect.* -import kotlin.reflect.full.* -import kotlin.reflect.jvm.jvmErasure - -/** - * Generates stdlib-index.json from Kotlin standard library using reflection. - * - * Run with: ./gradlew :stdlib-generator:generateStdlibIndex - */ -fun main(args: Array) { - val outputDir = if (args.isNotEmpty()) File(args[0]) else File(".") - outputDir.mkdirs() - - val generator = StdlibIndexGenerator() - val indexData = generator.generate() - - val json = Json { - prettyPrint = true - encodeDefaults = false - } - - val outputFile = File(outputDir, "stdlib-index.json") - outputFile.writeText(json.encodeToString(indexData)) - - println("Generated ${outputFile.absolutePath}") - println("Total symbols: ${indexData.totalCount}") - println("Classes: ${indexData.classes.size}") - println("Top-level functions: ${indexData.topLevelFunctions.size}") - println("Top-level properties: ${indexData.topLevelProperties.size}") - println("Extensions: ${indexData.extensions.values.sumOf { it.size }}") -} - -class StdlibIndexGenerator { - - private val processedClasses = mutableSetOf() - private val extractor = ReflectionSymbolExtractor() - - fun generate(): GeneratedIndexData { - val classes = mutableMapOf() - val topLevelFunctions = mutableListOf() - val topLevelProperties = mutableListOf() - val extensions = mutableMapOf>() - val typeAliases = mutableListOf() - - val stdlibPackages = listOf( - "kotlin", - "kotlin.collections", - "kotlin.comparisons", - "kotlin.io", - "kotlin.ranges", - "kotlin.sequences", - "kotlin.text", - "kotlin.math", - "kotlin.reflect" - ) - - for (packageName in stdlibPackages) { - processPackage(packageName, classes, topLevelFunctions, topLevelProperties, extensions) - } - - extractTopLevelFunctions(topLevelFunctions, extensions) - - processCoreTypes(classes) - addManualClasses(classes) - - return GeneratedIndexData( - version = "1.0", - kotlinVersion = KotlinVersion.CURRENT.toString(), - classes = classes, - topLevelFunctions = topLevelFunctions, - topLevelProperties = topLevelProperties, - extensions = extensions, - typeAliases = typeAliases - ) - } - - private fun extractTopLevelFunctions( - functions: MutableList, - extensions: MutableMap> - ) { - val facadeClasses = listOf( - "kotlin.io.ConsoleKt" to "kotlin.io", - "kotlin.StandardKt" to "kotlin", - "kotlin.PreconditionsKt" to "kotlin", - "kotlin.LateinitKt" to "kotlin", - "kotlin.TuplesKt" to "kotlin", - "kotlin.collections.CollectionsKt" to "kotlin.collections", - "kotlin.collections.MapsKt" to "kotlin.collections", - "kotlin.collections.SetsKt" to "kotlin.collections", - "kotlin.collections.ArraysKt" to "kotlin.collections", - "kotlin.collections.SequencesKt" to "kotlin.sequences", - "kotlin.text.StringsKt" to "kotlin.text", - "kotlin.text.CharsKt" to "kotlin.text", - "kotlin.ranges.RangesKt" to "kotlin.ranges", - "kotlin.comparisons.ComparisonsKt" to "kotlin.comparisons", - "kotlin.math.MathKt" to "kotlin.math" - ) - - for ((className, packageName) in facadeClasses) { - try { - val facadeClass = Class.forName(className) - extractFunctionsFromFacade(facadeClass, packageName, functions, extensions) - } catch (e: ClassNotFoundException) { - try { - val altClassName = "${className}__${className.substringAfterLast(".")}Kt" - val facadeClass = Class.forName(altClassName) - extractFunctionsFromFacade(facadeClass, packageName, functions, extensions) - } catch (e2: Exception) { - // Skip if class not found - } - } catch (e: Exception) { - System.err.println("Error processing facade $className: ${e.message}") - } - } - - addBuiltInFunctions(functions) - addBuiltInExtensions(extensions) - } - - private fun extractFunctionsFromFacade( - facadeClass: Class<*>, - packageName: String, - functions: MutableList, - extensions: MutableMap> - ) { - for (method in facadeClass.declaredMethods) { - if (!java.lang.reflect.Modifier.isPublic(method.modifiers)) continue - if (!java.lang.reflect.Modifier.isStatic(method.modifiers)) continue - - val name = method.name - if (name.contains("$")) continue - - val params = method.parameters.mapIndexed { index, param -> - GeneratedParamEntry( - name = param.name ?: "p$index", - type = param.type.kotlin.qualifiedName ?: "Any", - def = false, - vararg = param.isVarArgs - ) - } - - val returnType = method.returnType.kotlin.qualifiedName ?: "Unit" - val fqName = "$packageName.$name" - - val hasReceiver = params.isNotEmpty() && - method.parameterAnnotations.firstOrNull()?.any { - it.annotationClass.simpleName == "ExtensionFunctionType" - } == true - - if (hasReceiver && params.isNotEmpty()) { - val receiverType = params.first().type - val entry = GeneratedIndexEntry( - name = name, - fqName = fqName, - kind = "FUNCTION", - pkg = packageName, - params = params.drop(1), - ret = returnType, - recv = receiverType - ) - extensions.getOrPut(receiverType) { mutableListOf() }.add(entry) - } else { - val entry = GeneratedIndexEntry( - name = name, - fqName = fqName, - kind = "FUNCTION", - pkg = packageName, - params = params, - ret = returnType - ) - if (functions.none { it.name == name && it.pkg == packageName }) { - functions.add(entry) - } - } - } - } - - private data class BuiltInFunction( - val name: String, - val pkg: String, - val params: List>, - val returnType: String - ) - - private fun addBuiltInFunctions(functions: MutableList) { - val builtIns = listOf( - BuiltInFunction("println", "kotlin.io", listOf("message" to "Any?"), "Unit"), - BuiltInFunction("println", "kotlin.io", emptyList(), "Unit"), - BuiltInFunction("print", "kotlin.io", listOf("message" to "Any?"), "Unit"), - BuiltInFunction("readLine", "kotlin.io", emptyList(), "String?"), - BuiltInFunction("readln", "kotlin.io", emptyList(), "String"), - BuiltInFunction("listOf", "kotlin.collections", listOf("elements" to "Array"), "List"), - BuiltInFunction("listOf", "kotlin.collections", emptyList(), "List"), - BuiltInFunction("listOfNotNull", "kotlin.collections", listOf("elements" to "Array"), "List"), - BuiltInFunction("mutableListOf", "kotlin.collections", listOf("elements" to "Array"), "MutableList"), - BuiltInFunction("mutableListOf", "kotlin.collections", emptyList(), "MutableList"), - BuiltInFunction("arrayListOf", "kotlin.collections", listOf("elements" to "Array"), "ArrayList"), - BuiltInFunction("setOf", "kotlin.collections", listOf("elements" to "Array"), "Set"), - BuiltInFunction("setOf", "kotlin.collections", emptyList(), "Set"), - BuiltInFunction("mutableSetOf", "kotlin.collections", listOf("elements" to "Array"), "MutableSet"), - BuiltInFunction("hashSetOf", "kotlin.collections", listOf("elements" to "Array"), "HashSet"), - BuiltInFunction("linkedSetOf", "kotlin.collections", listOf("elements" to "Array"), "LinkedHashSet"), - BuiltInFunction("mapOf", "kotlin.collections", listOf("pairs" to "Array>"), "Map"), - BuiltInFunction("mapOf", "kotlin.collections", emptyList(), "Map"), - BuiltInFunction("mutableMapOf", "kotlin.collections", listOf("pairs" to "Array>"), "MutableMap"), - BuiltInFunction("hashMapOf", "kotlin.collections", listOf("pairs" to "Array>"), "HashMap"), - BuiltInFunction("linkedMapOf", "kotlin.collections", listOf("pairs" to "Array>"), "LinkedHashMap"), - BuiltInFunction("emptyList", "kotlin.collections", emptyList(), "List"), - BuiltInFunction("emptySet", "kotlin.collections", emptyList(), "Set"), - BuiltInFunction("emptyMap", "kotlin.collections", emptyList(), "Map"), - BuiltInFunction("emptyArray", "kotlin.collections", emptyList(), "Array"), - BuiltInFunction("arrayOf", "kotlin", listOf("elements" to "Array"), "Array"), - BuiltInFunction("intArrayOf", "kotlin", listOf("elements" to "IntArray"), "IntArray"), - BuiltInFunction("longArrayOf", "kotlin", listOf("elements" to "LongArray"), "LongArray"), - BuiltInFunction("floatArrayOf", "kotlin", listOf("elements" to "FloatArray"), "FloatArray"), - BuiltInFunction("doubleArrayOf", "kotlin", listOf("elements" to "DoubleArray"), "DoubleArray"), - BuiltInFunction("booleanArrayOf", "kotlin", listOf("elements" to "BooleanArray"), "BooleanArray"), - BuiltInFunction("charArrayOf", "kotlin", listOf("elements" to "CharArray"), "CharArray"), - BuiltInFunction("byteArrayOf", "kotlin", listOf("elements" to "ByteArray"), "ByteArray"), - BuiltInFunction("shortArrayOf", "kotlin", listOf("elements" to "ShortArray"), "ShortArray"), - BuiltInFunction("sequenceOf", "kotlin.sequences", listOf("elements" to "Array"), "Sequence"), - BuiltInFunction("emptySequence", "kotlin.sequences", emptyList(), "Sequence"), - BuiltInFunction("require", "kotlin", listOf("value" to "Boolean"), "Unit"), - BuiltInFunction("requireNotNull", "kotlin", listOf("value" to "T?"), "T"), - BuiltInFunction("check", "kotlin", listOf("value" to "Boolean"), "Unit"), - BuiltInFunction("checkNotNull", "kotlin", listOf("value" to "T?"), "T"), - BuiltInFunction("error", "kotlin", listOf("message" to "Any"), "Nothing"), - BuiltInFunction("TODO", "kotlin", listOf("reason" to "String"), "Nothing"), - BuiltInFunction("TODO", "kotlin", emptyList(), "Nothing"), - BuiltInFunction("run", "kotlin", listOf("block" to "() -> R"), "R"), - BuiltInFunction("with", "kotlin", listOf("receiver" to "T", "block" to "T.() -> R"), "R"), - BuiltInFunction("apply", "kotlin", listOf("block" to "T.() -> Unit"), "T"), - BuiltInFunction("also", "kotlin", listOf("block" to "(T) -> Unit"), "T"), - BuiltInFunction("let", "kotlin", listOf("block" to "(T) -> R"), "R"), - BuiltInFunction("takeIf", "kotlin", listOf("predicate" to "(T) -> Boolean"), "T?"), - BuiltInFunction("takeUnless", "kotlin", listOf("predicate" to "(T) -> Boolean"), "T?"), - BuiltInFunction("repeat", "kotlin", listOf("times" to "Int", "action" to "(Int) -> Unit"), "Unit"), - BuiltInFunction("lazy", "kotlin", listOf("initializer" to "() -> T"), "Lazy"), - BuiltInFunction("lazyOf", "kotlin", listOf("value" to "T"), "Lazy"), - BuiltInFunction("buildString", "kotlin.text", listOf("builderAction" to "StringBuilder.() -> Unit"), "String"), - BuiltInFunction("buildList", "kotlin.collections", listOf("builderAction" to "MutableList.() -> Unit"), "List"), - BuiltInFunction("buildSet", "kotlin.collections", listOf("builderAction" to "MutableSet.() -> Unit"), "Set"), - BuiltInFunction("buildMap", "kotlin.collections", listOf("builderAction" to "MutableMap.() -> Unit"), "Map"), - BuiltInFunction("maxOf", "kotlin.comparisons", listOf("a" to "T", "b" to "T"), "T"), - BuiltInFunction("minOf", "kotlin.comparisons", listOf("a" to "T", "b" to "T"), "T"), - BuiltInFunction("to", "kotlin", listOf("that" to "B"), "Pair"), - BuiltInFunction("Pair", "kotlin", listOf("first" to "A", "second" to "B"), "Pair"), - BuiltInFunction("Triple", "kotlin", listOf("first" to "A", "second" to "B", "third" to "C"), "Triple") - ) - - for (func in builtIns) { - if (functions.any { it.name == func.name && it.pkg == func.pkg && it.params.size == func.params.size }) continue - - val params = func.params.map { (pName, pType) -> - GeneratedParamEntry(name = pName, type = pType) - } - - functions.add(GeneratedIndexEntry( - name = func.name, - fqName = "${func.pkg}.${func.name}", - kind = "FUNCTION", - pkg = func.pkg, - params = params, - ret = func.returnType - )) - } - } - - private fun addBuiltInExtensions(extensions: MutableMap>) { - val iterableExtensions = mapOf( - "forEach" to "Unit", - "forEachIndexed" to "Unit", - "map" to "List", - "mapNotNull" to "List", - "mapIndexed" to "List", - "filter" to "List", - "filterNot" to "List", - "filterNotNull" to "List", - "flatMap" to "List", - "flatMapIndexed" to "List", - "fold" to "R", - "reduce" to "T", - "first" to "T", - "firstOrNull" to "T?", - "last" to "T", - "lastOrNull" to "T?", - "single" to "T", - "singleOrNull" to "T?", - "find" to "T?", - "findLast" to "T?", - "any" to "Boolean", - "all" to "Boolean", - "none" to "Boolean", - "count" to "Int", - "toList" to "List", - "toMutableList" to "MutableList", - "toSet" to "Set", - "toMutableSet" to "MutableSet", - "sorted" to "List", - "sortedBy" to "List", - "sortedDescending" to "List", - "sortedByDescending" to "List", - "reversed" to "List", - "shuffled" to "List", - "distinct" to "List", - "distinctBy" to "List", - "take" to "List", - "takeLast" to "List", - "takeWhile" to "List", - "takeLastWhile" to "List", - "drop" to "List", - "dropLast" to "List", - "dropWhile" to "List", - "dropLastWhile" to "List", - "zip" to "List>", - "zipWithNext" to "List", - "partition" to "Pair, List>", - "groupBy" to "Map>", - "associate" to "Map", - "associateBy" to "Map", - "associateWith" to "Map", - "joinToString" to "String", - "joinTo" to "A", - "sum" to "Int", - "sumOf" to "R", - "average" to "Double", - "max" to "T", - "maxOrNull" to "T?", - "min" to "T", - "minOrNull" to "T?", - "maxBy" to "T", - "maxByOrNull" to "T?", - "minBy" to "T", - "minByOrNull" to "T?", - "contains" to "Boolean", - "indexOf" to "Int", - "lastIndexOf" to "Int", - "isEmpty" to "Boolean", - "isNotEmpty" to "Boolean", - "plus" to "List", - "minus" to "List", - "plusElement" to "List", - "minusElement" to "List", - "onEach" to "C", - "onEachIndexed" to "C", - "asSequence" to "Sequence", - "asIterable" to "Iterable", - "iterator" to "Iterator", - "withIndex" to "Iterable>", - "chunked" to "List>", - "windowed" to "List>", - "unzip" to "Pair, List>" - ) - - val stringExtensions = mapOf( - "trim" to "String", - "trimStart" to "String", - "trimEnd" to "String", - "padStart" to "String", - "padEnd" to "String", - "split" to "List", - "lines" to "List", - "replace" to "String", - "replaceFirst" to "String", - "replaceRange" to "String", - "substring" to "String", - "substringBefore" to "String", - "substringAfter" to "String", - "substringBeforeLast" to "String", - "substringAfterLast" to "String", - "startsWith" to "Boolean", - "endsWith" to "Boolean", - "removePrefix" to "String", - "removeSuffix" to "String", - "uppercase" to "String", - "lowercase" to "String", - "capitalize" to "String", - "decapitalize" to "String", - "toInt" to "Int", - "toLong" to "Long", - "toFloat" to "Float", - "toDouble" to "Double", - "toBoolean" to "Boolean", - "toIntOrNull" to "Int?", - "toLongOrNull" to "Long?", - "toFloatOrNull" to "Float?", - "toDoubleOrNull" to "Double?", - "toByteArray" to "ByteArray", - "toCharArray" to "CharArray", - "isBlank" to "Boolean", - "isNotBlank" to "Boolean", - "isEmpty" to "Boolean", - "isNotEmpty" to "Boolean", - "isNullOrBlank" to "Boolean", - "isNullOrEmpty" to "Boolean", - "orEmpty" to "String", - "reversed" to "String", - "repeat" to "String", - "format" to "String", - "contains" to "Boolean", - "indexOf" to "Int", - "lastIndexOf" to "Int", - "first" to "Char", - "last" to "Char", - "single" to "Char", - "take" to "String", - "drop" to "String", - "filter" to "String", - "map" to "List", - "flatMap" to "List", - "forEach" to "Unit" - ) - - val mapExtensions = mapOf( - "get" to "V?", - "getValue" to "V", - "getOrDefault" to "V", - "getOrElse" to "V", - "getOrPut" to "V", - "keys" to "Set", - "values" to "Collection", - "entries" to "Set>", - "containsKey" to "Boolean", - "containsValue" to "Boolean", - "forEach" to "Unit", - "map" to "List", - "mapKeys" to "Map", - "mapValues" to "Map", - "filter" to "Map", - "filterKeys" to "Map", - "filterValues" to "Map", - "filterNot" to "Map", - "toList" to "List>", - "toMutableMap" to "MutableMap", - "plus" to "Map", - "minus" to "Map", - "isEmpty" to "Boolean", - "isNotEmpty" to "Boolean", - "any" to "Boolean", - "all" to "Boolean", - "none" to "Boolean", - "count" to "Int" - ) - - val collectionExtensions = mapOf( - "size" to "Int", - "indices" to "IntRange", - "lastIndex" to "Int", - "isEmpty" to "Boolean", - "isNotEmpty" to "Boolean", - "contains" to "Boolean", - "containsAll" to "Boolean", - "random" to "T", - "randomOrNull" to "T?" - ) - - val anyExtensions = mapOf( - "toString" to "String", - "hashCode" to "Int", - "equals" to "Boolean", - "let" to "R", - "run" to "R", - "apply" to "T", - "also" to "T", - "takeIf" to "T?", - "takeUnless" to "T?" - ) - - fun addExtensions(receiverType: String, extensionMap: Map, pkg: String = "kotlin.collections") { - val list = extensions.getOrPut(receiverType) { mutableListOf() } - for ((name, returnType) in extensionMap) { - if (list.any { it.name == name }) continue - list.add(GeneratedIndexEntry( - name = name, - fqName = "$pkg.$name", - kind = "FUNCTION", - pkg = pkg, - recv = receiverType, - ret = returnType - )) - } - } - - addExtensions("kotlin.collections.Iterable", iterableExtensions) - addExtensions("Iterable", iterableExtensions) - addExtensions("kotlin.collections.Collection", collectionExtensions) - addExtensions("Collection", collectionExtensions) - addExtensions("kotlin.collections.List", iterableExtensions + collectionExtensions) - addExtensions("List", iterableExtensions + collectionExtensions) - addExtensions("kotlin.collections.MutableList", iterableExtensions + collectionExtensions) - addExtensions("MutableList", iterableExtensions + collectionExtensions) - addExtensions("kotlin.collections.Set", iterableExtensions + collectionExtensions) - addExtensions("Set", iterableExtensions + collectionExtensions) - addExtensions("kotlin.collections.Map", mapExtensions) - addExtensions("Map", mapExtensions) - addExtensions("kotlin.String", stringExtensions, "kotlin.text") - addExtensions("String", stringExtensions, "kotlin.text") - addExtensions("kotlin.CharSequence", stringExtensions, "kotlin.text") - addExtensions("CharSequence", stringExtensions, "kotlin.text") - addExtensions("kotlin.Any", anyExtensions, "kotlin") - addExtensions("Any", anyExtensions, "kotlin") - addExtensions("kotlin.Array", iterableExtensions + collectionExtensions) - addExtensions("Array", iterableExtensions + collectionExtensions) - - val primitiveArrayExtensions = mapOf( - "sum" to "Int", - "average" to "Double", - "min" to "Int", - "max" to "Int", - "minOrNull" to "Int?", - "maxOrNull" to "Int?", - "forEach" to "Unit", - "forEachIndexed" to "Unit", - "map" to "List", - "filter" to "List", - "first" to "Int", - "firstOrNull" to "Int?", - "last" to "Int", - "lastOrNull" to "Int?", - "contains" to "Boolean", - "indexOf" to "Int", - "lastIndexOf" to "Int", - "toList" to "List", - "toMutableList" to "MutableList", - "toSet" to "Set", - "sorted" to "List", - "sortedDescending" to "List", - "reversed" to "List", - "distinct" to "List", - "any" to "Boolean", - "all" to "Boolean", - "none" to "Boolean", - "count" to "Int", - "isEmpty" to "Boolean", - "isNotEmpty" to "Boolean", - "asList" to "List", - "asIterable" to "Iterable", - "asSequence" to "Sequence", - "copyOf" to "IntArray", - "copyOfRange" to "IntArray", - "sliceArray" to "IntArray", - "sortedArray" to "IntArray", - "sortedArrayDescending" to "IntArray" - ) - - addExtensions("kotlin.IntArray", iterableExtensions + collectionExtensions + primitiveArrayExtensions) - addExtensions("IntArray", iterableExtensions + collectionExtensions + primitiveArrayExtensions) - - val longArrayExtensions = primitiveArrayExtensions.mapValues { (k, v) -> - v.replace("Int", "Long") - } - addExtensions("kotlin.LongArray", iterableExtensions + collectionExtensions + longArrayExtensions) - addExtensions("LongArray", iterableExtensions + collectionExtensions + longArrayExtensions) - - val doubleArrayExtensions = primitiveArrayExtensions.mapValues { (k, v) -> - v.replace("Int", "Double") - } - addExtensions("kotlin.DoubleArray", iterableExtensions + collectionExtensions + doubleArrayExtensions) - addExtensions("DoubleArray", iterableExtensions + collectionExtensions + doubleArrayExtensions) - - val floatArrayExtensions = primitiveArrayExtensions.mapValues { (k, v) -> - when { - v == "Int" -> "Float" - v.contains("Int") -> v.replace("Int", "Float") - else -> v - } - } - addExtensions("kotlin.FloatArray", iterableExtensions + collectionExtensions + floatArrayExtensions) - addExtensions("FloatArray", iterableExtensions + collectionExtensions + floatArrayExtensions) - - addExtensions("kotlin.Sequence", iterableExtensions) - addExtensions("Sequence", iterableExtensions) - } - - private fun processPackage( - packageName: String, - classes: MutableMap, - functions: MutableList, - properties: MutableList, - extensions: MutableMap> - ) { - val packageClasses = getClassesInPackage(packageName) - - for (kClass in packageClasses) { - try { - processClass(kClass, classes, extensions) - } catch (e: Exception) { - System.err.println("Error processing ${kClass.qualifiedName}: ${e.message}") - } - } - } - - private fun getClassesInPackage(packageName: String): List> { - val coreTypes = when (packageName) { - "kotlin" -> listOf( - Any::class, Nothing::class, Unit::class, - Boolean::class, Byte::class, Short::class, Int::class, Long::class, - Float::class, Double::class, Char::class, - String::class, CharSequence::class, - Number::class, Comparable::class, - Throwable::class, Exception::class, Error::class, - RuntimeException::class, IllegalArgumentException::class, - IllegalStateException::class, NullPointerException::class, - IndexOutOfBoundsException::class, NoSuchElementException::class, - UnsupportedOperationException::class, ConcurrentModificationException::class, - ClassCastException::class, ArithmeticException::class, - NumberFormatException::class, AssertionError::class, - Pair::class, Triple::class, - Lazy::class, Result::class, - Function::class, KClass::class, - Enum::class, Annotation::class, - Cloneable::class - ) - "kotlin.collections" -> listOf( - Iterable::class, MutableIterable::class, - Collection::class, MutableCollection::class, - List::class, MutableList::class, - Set::class, MutableSet::class, - Map::class, MutableMap::class, - Map.Entry::class, MutableMap.MutableEntry::class, - Iterator::class, MutableIterator::class, - ListIterator::class, MutableListIterator::class, - ArrayList::class, LinkedHashSet::class, LinkedHashMap::class, - HashSet::class, HashMap::class, - ArrayDeque::class - ) - "kotlin.ranges" -> listOf( - IntRange::class, LongRange::class, CharRange::class, - IntProgression::class, LongProgression::class, CharProgression::class, - ClosedRange::class - ) - "kotlin.sequences" -> listOf( - Sequence::class - ) - "kotlin.text" -> listOf( - Regex::class, MatchResult::class, MatchGroup::class, - StringBuilder::class, Appendable::class - ) - else -> emptyList() - } - return coreTypes - } - - private fun addManualClasses(classes: MutableMap) { - val manualClasses = listOf( - GeneratedClassEntry( - fqName = "kotlin.text.StringBuilder", - kind = "CLASS", - supers = listOf("kotlin.CharSequence", "kotlin.text.Appendable"), - members = listOf( - GeneratedMemberEntry("append", "FUNCTION", "fun append(value: Any?): StringBuilder", listOf(GeneratedParamEntry("value", "Any?")), "StringBuilder"), - GeneratedMemberEntry("append", "FUNCTION", "fun append(value: String?): StringBuilder", listOf(GeneratedParamEntry("value", "String?")), "StringBuilder"), - GeneratedMemberEntry("append", "FUNCTION", "fun append(value: Char): StringBuilder", listOf(GeneratedParamEntry("value", "Char")), "StringBuilder"), - GeneratedMemberEntry("append", "FUNCTION", "fun append(value: Int): StringBuilder", listOf(GeneratedParamEntry("value", "Int")), "StringBuilder"), - GeneratedMemberEntry("append", "FUNCTION", "fun append(value: Long): StringBuilder", listOf(GeneratedParamEntry("value", "Long")), "StringBuilder"), - GeneratedMemberEntry("append", "FUNCTION", "fun append(value: Boolean): StringBuilder", listOf(GeneratedParamEntry("value", "Boolean")), "StringBuilder"), - GeneratedMemberEntry("appendLine", "FUNCTION", "fun appendLine(): StringBuilder", emptyList(), "StringBuilder"), - GeneratedMemberEntry("appendLine", "FUNCTION", "fun appendLine(value: Any?): StringBuilder", listOf(GeneratedParamEntry("value", "Any?")), "StringBuilder"), - GeneratedMemberEntry("insert", "FUNCTION", "fun insert(index: Int, value: Any?): StringBuilder", listOf(GeneratedParamEntry("index", "Int"), GeneratedParamEntry("value", "Any?")), "StringBuilder"), - GeneratedMemberEntry("delete", "FUNCTION", "fun delete(startIndex: Int, endIndex: Int): StringBuilder", listOf(GeneratedParamEntry("startIndex", "Int"), GeneratedParamEntry("endIndex", "Int")), "StringBuilder"), - GeneratedMemberEntry("deleteAt", "FUNCTION", "fun deleteAt(index: Int): StringBuilder", listOf(GeneratedParamEntry("index", "Int")), "StringBuilder"), - GeneratedMemberEntry("clear", "FUNCTION", "fun clear(): StringBuilder", emptyList(), "StringBuilder"), - GeneratedMemberEntry("reverse", "FUNCTION", "fun reverse(): StringBuilder", emptyList(), "StringBuilder"), - GeneratedMemberEntry("setLength", "FUNCTION", "fun setLength(newLength: Int): Unit", listOf(GeneratedParamEntry("newLength", "Int")), "Unit"), - GeneratedMemberEntry("length", "PROPERTY", "val length: Int", emptyList(), "Int"), - GeneratedMemberEntry("capacity", "PROPERTY", "val capacity: Int", emptyList(), "Int"), - GeneratedMemberEntry("toString", "FUNCTION", "fun toString(): String", emptyList(), "String"), - GeneratedMemberEntry("substring", "FUNCTION", "fun substring(startIndex: Int): String", listOf(GeneratedParamEntry("startIndex", "Int")), "String"), - GeneratedMemberEntry("substring", "FUNCTION", "fun substring(startIndex: Int, endIndex: Int): String", listOf(GeneratedParamEntry("startIndex", "Int"), GeneratedParamEntry("endIndex", "Int")), "String") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.text.Appendable", - kind = "INTERFACE", - members = listOf( - GeneratedMemberEntry("append", "FUNCTION", "fun append(value: Char): Appendable", listOf(GeneratedParamEntry("value", "Char")), "Appendable"), - GeneratedMemberEntry("append", "FUNCTION", "fun append(value: CharSequence?): Appendable", listOf(GeneratedParamEntry("value", "CharSequence?")), "Appendable") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.ArrayList", - kind = "CLASS", - typeParams = listOf("E"), - supers = listOf("kotlin.collections.MutableList"), - members = listOf( - GeneratedMemberEntry("add", "FUNCTION", "fun add(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("add", "FUNCTION", "fun add(index: Int, element: E): Unit", listOf(GeneratedParamEntry("index", "Int"), GeneratedParamEntry("element", "E")), "Unit"), - GeneratedMemberEntry("addAll", "FUNCTION", "fun addAll(elements: Collection): Boolean", listOf(GeneratedParamEntry("elements", "Collection")), "Boolean"), - GeneratedMemberEntry("remove", "FUNCTION", "fun remove(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("removeAt", "FUNCTION", "fun removeAt(index: Int): E", listOf(GeneratedParamEntry("index", "Int")), "E"), - GeneratedMemberEntry("clear", "FUNCTION", "fun clear(): Unit", emptyList(), "Unit"), - GeneratedMemberEntry("get", "FUNCTION", "fun get(index: Int): E", listOf(GeneratedParamEntry("index", "Int")), "E"), - GeneratedMemberEntry("set", "FUNCTION", "fun set(index: Int, element: E): E", listOf(GeneratedParamEntry("index", "Int"), GeneratedParamEntry("element", "E")), "E"), - GeneratedMemberEntry("size", "PROPERTY", "val size: Int", emptyList(), "Int"), - GeneratedMemberEntry("isEmpty", "FUNCTION", "fun isEmpty(): Boolean", emptyList(), "Boolean"), - GeneratedMemberEntry("contains", "FUNCTION", "fun contains(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("indexOf", "FUNCTION", "fun indexOf(element: E): Int", listOf(GeneratedParamEntry("element", "E")), "Int"), - GeneratedMemberEntry("lastIndexOf", "FUNCTION", "fun lastIndexOf(element: E): Int", listOf(GeneratedParamEntry("element", "E")), "Int") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.HashMap", - kind = "CLASS", - typeParams = listOf("K", "V"), - supers = listOf("kotlin.collections.MutableMap"), - members = listOf( - GeneratedMemberEntry("put", "FUNCTION", "fun put(key: K, value: V): V?", listOf(GeneratedParamEntry("key", "K"), GeneratedParamEntry("value", "V")), "V?"), - GeneratedMemberEntry("get", "FUNCTION", "fun get(key: K): V?", listOf(GeneratedParamEntry("key", "K")), "V?"), - GeneratedMemberEntry("remove", "FUNCTION", "fun remove(key: K): V?", listOf(GeneratedParamEntry("key", "K")), "V?"), - GeneratedMemberEntry("clear", "FUNCTION", "fun clear(): Unit", emptyList(), "Unit"), - GeneratedMemberEntry("containsKey", "FUNCTION", "fun containsKey(key: K): Boolean", listOf(GeneratedParamEntry("key", "K")), "Boolean"), - GeneratedMemberEntry("containsValue", "FUNCTION", "fun containsValue(value: V): Boolean", listOf(GeneratedParamEntry("value", "V")), "Boolean"), - GeneratedMemberEntry("size", "PROPERTY", "val size: Int", emptyList(), "Int"), - GeneratedMemberEntry("isEmpty", "FUNCTION", "fun isEmpty(): Boolean", emptyList(), "Boolean"), - GeneratedMemberEntry("keys", "PROPERTY", "val keys: MutableSet", emptyList(), "MutableSet"), - GeneratedMemberEntry("values", "PROPERTY", "val values: MutableCollection", emptyList(), "MutableCollection"), - GeneratedMemberEntry("entries", "PROPERTY", "val entries: MutableSet>", emptyList(), "MutableSet>") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.HashSet", - kind = "CLASS", - typeParams = listOf("E"), - supers = listOf("kotlin.collections.MutableSet"), - members = listOf( - GeneratedMemberEntry("add", "FUNCTION", "fun add(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("remove", "FUNCTION", "fun remove(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("clear", "FUNCTION", "fun clear(): Unit", emptyList(), "Unit"), - GeneratedMemberEntry("contains", "FUNCTION", "fun contains(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("size", "PROPERTY", "val size: Int", emptyList(), "Int"), - GeneratedMemberEntry("isEmpty", "FUNCTION", "fun isEmpty(): Boolean", emptyList(), "Boolean") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.LinkedHashMap", - kind = "CLASS", - typeParams = listOf("K", "V"), - supers = listOf("kotlin.collections.HashMap") - ), - GeneratedClassEntry( - fqName = "kotlin.collections.LinkedHashSet", - kind = "CLASS", - typeParams = listOf("E"), - supers = listOf("kotlin.collections.HashSet") - ), - GeneratedClassEntry( - fqName = "kotlin.collections.MutableList", - kind = "INTERFACE", - typeParams = listOf("E"), - supers = listOf("kotlin.collections.List", "kotlin.collections.MutableCollection"), - members = listOf( - GeneratedMemberEntry("add", "FUNCTION", "fun add(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("add", "FUNCTION", "fun add(index: Int, element: E): Unit", listOf(GeneratedParamEntry("index", "Int"), GeneratedParamEntry("element", "E")), "Unit"), - GeneratedMemberEntry("addAll", "FUNCTION", "fun addAll(elements: Collection): Boolean", listOf(GeneratedParamEntry("elements", "Collection")), "Boolean"), - GeneratedMemberEntry("remove", "FUNCTION", "fun remove(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("removeAt", "FUNCTION", "fun removeAt(index: Int): E", listOf(GeneratedParamEntry("index", "Int")), "E"), - GeneratedMemberEntry("set", "FUNCTION", "fun set(index: Int, element: E): E", listOf(GeneratedParamEntry("index", "Int"), GeneratedParamEntry("element", "E")), "E"), - GeneratedMemberEntry("clear", "FUNCTION", "fun clear(): Unit", emptyList(), "Unit") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.MutableSet", - kind = "INTERFACE", - typeParams = listOf("E"), - supers = listOf("kotlin.collections.Set", "kotlin.collections.MutableCollection"), - members = listOf( - GeneratedMemberEntry("add", "FUNCTION", "fun add(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("remove", "FUNCTION", "fun remove(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("clear", "FUNCTION", "fun clear(): Unit", emptyList(), "Unit") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.MutableMap", - kind = "INTERFACE", - typeParams = listOf("K", "V"), - supers = listOf("kotlin.collections.Map"), - members = listOf( - GeneratedMemberEntry("put", "FUNCTION", "fun put(key: K, value: V): V?", listOf(GeneratedParamEntry("key", "K"), GeneratedParamEntry("value", "V")), "V?"), - GeneratedMemberEntry("putAll", "FUNCTION", "fun putAll(from: Map): Unit", listOf(GeneratedParamEntry("from", "Map")), "Unit"), - GeneratedMemberEntry("remove", "FUNCTION", "fun remove(key: K): V?", listOf(GeneratedParamEntry("key", "K")), "V?"), - GeneratedMemberEntry("clear", "FUNCTION", "fun clear(): Unit", emptyList(), "Unit"), - GeneratedMemberEntry("keys", "PROPERTY", "val keys: MutableSet", emptyList(), "MutableSet"), - GeneratedMemberEntry("values", "PROPERTY", "val values: MutableCollection", emptyList(), "MutableCollection"), - GeneratedMemberEntry("entries", "PROPERTY", "val entries: MutableSet>", emptyList(), "MutableSet>") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.MutableCollection", - kind = "INTERFACE", - typeParams = listOf("E"), - supers = listOf("kotlin.collections.Collection", "kotlin.collections.MutableIterable"), - members = listOf( - GeneratedMemberEntry("add", "FUNCTION", "fun add(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("addAll", "FUNCTION", "fun addAll(elements: Collection): Boolean", listOf(GeneratedParamEntry("elements", "Collection")), "Boolean"), - GeneratedMemberEntry("remove", "FUNCTION", "fun remove(element: E): Boolean", listOf(GeneratedParamEntry("element", "E")), "Boolean"), - GeneratedMemberEntry("removeAll", "FUNCTION", "fun removeAll(elements: Collection): Boolean", listOf(GeneratedParamEntry("elements", "Collection")), "Boolean"), - GeneratedMemberEntry("retainAll", "FUNCTION", "fun retainAll(elements: Collection): Boolean", listOf(GeneratedParamEntry("elements", "Collection")), "Boolean"), - GeneratedMemberEntry("clear", "FUNCTION", "fun clear(): Unit", emptyList(), "Unit") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.MutableIterable", - kind = "INTERFACE", - typeParams = listOf("T"), - supers = listOf("kotlin.collections.Iterable"), - members = listOf( - GeneratedMemberEntry("iterator", "FUNCTION", "fun iterator(): MutableIterator", emptyList(), "MutableIterator") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.MutableIterator", - kind = "INTERFACE", - typeParams = listOf("T"), - supers = listOf("kotlin.collections.Iterator"), - members = listOf( - GeneratedMemberEntry("remove", "FUNCTION", "fun remove(): Unit", emptyList(), "Unit") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.collections.ArrayDeque", - kind = "CLASS", - typeParams = listOf("E"), - supers = listOf("kotlin.collections.MutableList"), - members = listOf( - GeneratedMemberEntry("addFirst", "FUNCTION", "fun addFirst(element: E): Unit", listOf(GeneratedParamEntry("element", "E")), "Unit"), - GeneratedMemberEntry("addLast", "FUNCTION", "fun addLast(element: E): Unit", listOf(GeneratedParamEntry("element", "E")), "Unit"), - GeneratedMemberEntry("removeFirst", "FUNCTION", "fun removeFirst(): E", emptyList(), "E"), - GeneratedMemberEntry("removeLast", "FUNCTION", "fun removeLast(): E", emptyList(), "E"), - GeneratedMemberEntry("first", "FUNCTION", "fun first(): E", emptyList(), "E"), - GeneratedMemberEntry("last", "FUNCTION", "fun last(): E", emptyList(), "E"), - GeneratedMemberEntry("size", "PROPERTY", "val size: Int", emptyList(), "Int") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.Enum", - kind = "CLASS", - typeParams = listOf("E"), - supers = listOf("kotlin.Comparable"), - members = listOf( - GeneratedMemberEntry("name", "PROPERTY", "val name: String", emptyList(), "String"), - GeneratedMemberEntry("ordinal", "PROPERTY", "val ordinal: Int", emptyList(), "Int") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.Annotation", - kind = "INTERFACE" - ), - GeneratedClassEntry( - fqName = "kotlin.Cloneable", - kind = "INTERFACE" - ), - GeneratedClassEntry( - fqName = "kotlin.Nothing", - kind = "CLASS" - ), - GeneratedClassEntry( - fqName = "kotlin.RuntimeException", - kind = "CLASS", - supers = listOf("kotlin.Exception"), - members = listOf( - GeneratedMemberEntry("message", "PROPERTY", "val message: String?", emptyList(), "String?"), - GeneratedMemberEntry("cause", "PROPERTY", "val cause: Throwable?", emptyList(), "Throwable?") - ) - ), - GeneratedClassEntry( - fqName = "kotlin.Exception", - kind = "CLASS", - supers = listOf("kotlin.Throwable") - ), - GeneratedClassEntry( - fqName = "kotlin.Error", - kind = "CLASS", - supers = listOf("kotlin.Throwable") - ), - GeneratedClassEntry( - fqName = "kotlin.IllegalArgumentException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.IllegalStateException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.NullPointerException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.IndexOutOfBoundsException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.NoSuchElementException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.UnsupportedOperationException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.ConcurrentModificationException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.ClassCastException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.ArithmeticException", - kind = "CLASS", - supers = listOf("kotlin.RuntimeException") - ), - GeneratedClassEntry( - fqName = "kotlin.NumberFormatException", - kind = "CLASS", - supers = listOf("kotlin.IllegalArgumentException") - ), - GeneratedClassEntry( - fqName = "kotlin.AssertionError", - kind = "CLASS", - supers = listOf("kotlin.Error") - ) - ) - - for (classEntry in manualClasses) { - if (classEntry.fqName !in classes) { - classes[classEntry.fqName] = classEntry - } - } - } - - private fun processCoreTypes(classes: MutableMap) { - val primitiveTypes = mapOf( - "kotlin.Int" to listOf("kotlin.Number", "kotlin.Comparable"), - "kotlin.Long" to listOf("kotlin.Number", "kotlin.Comparable"), - "kotlin.Short" to listOf("kotlin.Number", "kotlin.Comparable"), - "kotlin.Byte" to listOf("kotlin.Number", "kotlin.Comparable"), - "kotlin.Float" to listOf("kotlin.Number", "kotlin.Comparable"), - "kotlin.Double" to listOf("kotlin.Number", "kotlin.Comparable"), - "kotlin.Char" to listOf("kotlin.Comparable"), - "kotlin.Boolean" to listOf("kotlin.Comparable") - ) - - for ((fqName, supers) in primitiveTypes) { - val existing = classes[fqName] - if (existing != null) { - classes[fqName] = existing.copy(supers = supers) - } - } - } - - private fun processClass( - kClass: KClass<*>, - classes: MutableMap, - extensions: MutableMap> - ) { - val fqName = kClass.qualifiedName ?: return - if (fqName in processedClasses) return - if (!fqName.startsWith("kotlin")) return - processedClasses.add(fqName) - - val classEntry = extractor.extractClass(kClass) - if (classEntry != null) { - classes[fqName] = classEntry - } - - try { - for (member in kClass.memberExtensionFunctions) { - try { - val entry = extractor.extractExtension(member) - if (entry != null) { - val receiverType = entry.recv ?: continue - extensions.getOrPut(receiverType) { mutableListOf() }.add(entry) - } - } catch (e: Exception) { - // Skip problematic extension - } - } - } catch (e: Exception) { - // Skip if you can't access extensions - } - } -} - -class ReflectionSymbolExtractor { - - fun extractClass(kClass: KClass<*>): GeneratedClassEntry? { - val fqName = kClass.qualifiedName ?: return null - val simpleName = kClass.simpleName ?: return null - - val kind = when { - kClass.isData -> "DATA_CLASS" - kClass.isValue -> "VALUE_CLASS" - kClass.isSealed -> "CLASS" - kClass.java.isInterface -> "INTERFACE" - kClass.java.isEnum -> "ENUM_CLASS" - kClass.java.isAnnotation -> "ANNOTATION_CLASS" - kClass.objectInstance != null -> "OBJECT" - else -> "CLASS" - } - - val typeParams = try { - kClass.typeParameters.map { it.name } - } catch (e: Exception) { - emptyList() - } - - val supers = try { - kClass.supertypes - .mapNotNull { renderType(it) } - .filter { it != "kotlin.Any" } - } catch (e: Exception) { - emptyList() - } - - val members = mutableListOf() - - try { - for (func in kClass.memberFunctions) { - if (!func.visibility.isPublicApi()) continue - val member = extractMember(func) - if (member != null) members.add(member) - } - } catch (e: Exception) { - // Skip members on error - } - - try { - for (prop in kClass.memberProperties) { - if (!prop.visibility.isPublicApi()) continue - val member = extractProperty(prop) - if (member != null) members.add(member) - } - } catch (e: Exception) { - // Skip properties on error - } - - val isDeprecated = kClass.annotations.any { it.annotationClass == Deprecated::class } - val deprecationMessage = kClass.annotations - .filterIsInstance() - .firstOrNull()?.message - - return GeneratedClassEntry( - fqName = fqName, - kind = kind, - typeParams = typeParams, - supers = supers, - members = members, - companions = emptyList(), - nested = emptyList(), - dep = isDeprecated, - depMsg = deprecationMessage - ) - } - - fun extractExtension(func: KFunction<*>): GeneratedIndexEntry? { - val name = func.name - val extensionReceiver = func.extensionReceiverParameter ?: return null - val receiverType = renderType(extensionReceiver.type) ?: return null - - val params = func.parameters - .filter { it.kind == KParameter.Kind.VALUE } - .map { param -> - GeneratedParamEntry( - name = param.name ?: "p${param.index}", - type = renderType(param.type) ?: "Any", - def = param.isOptional, - vararg = param.isVararg - ) - } - - val returnType = renderType(func.returnType) - val typeParams = func.typeParameters.map { it.name } - - val packageName = func.javaClass.`package`?.name ?: "kotlin" - val fqName = "$packageName.$name" - - return GeneratedIndexEntry( - name = name, - fqName = fqName, - kind = "FUNCTION", - pkg = packageName, - typeParams = typeParams, - params = params, - ret = returnType, - recv = receiverType - ) - } - - private fun extractMember(func: KFunction<*>): GeneratedMemberEntry? { - val name = func.name - if (name.startsWith("component") && name.length == 10) return null - if (name in setOf("copy", "equals", "hashCode", "toString")) return null - - val params = func.parameters - .filter { it.kind == KParameter.Kind.VALUE } - .map { param -> - GeneratedParamEntry( - name = param.name ?: "p${param.index}", - type = renderType(param.type) ?: "Any", - def = param.isOptional, - vararg = param.isVararg - ) - } - - val returnType = renderType(func.returnType) - val visibility = func.visibility?.name ?: "PUBLIC" - - val isDeprecated = func.annotations.any { it.annotationClass == Deprecated::class } - - val signature = buildString { - append("fun ") - if (func.typeParameters.isNotEmpty()) { - append("<") - append(func.typeParameters.joinToString { it.name }) - append("> ") - } - append(name) - append("(") - append(params.joinToString { "${it.name}: ${it.type}" }) - append(")") - returnType?.let { append(": $it") } - } - - return GeneratedMemberEntry( - name = name, - kind = "FUNCTION", - sig = signature, - params = params, - ret = returnType, - vis = visibility, - dep = isDeprecated - ) - } - - private fun extractProperty(prop: KProperty<*>): GeneratedMemberEntry? { - val name = prop.name - val type = renderType(prop.returnType) - val visibility = prop.visibility?.name ?: "PUBLIC" - val isDeprecated = prop.annotations.any { it.annotationClass == Deprecated::class } - - val signature = buildString { - append(if (prop is KMutableProperty<*>) "var " else "val ") - append(name) - type?.let { append(": $it") } - } - - return GeneratedMemberEntry( - name = name, - kind = "PROPERTY", - sig = signature, - params = emptyList(), - ret = type, - vis = visibility, - dep = isDeprecated - ) - } - - private fun renderType(type: KType): String? { - val classifier = type.classifier ?: return null - - val baseName = when (classifier) { - is KClass<*> -> classifier.qualifiedName ?: classifier.simpleName ?: return null - is KTypeParameter -> classifier.name - else -> return null - } - - val result = buildString { - append(baseName) - - val args = type.arguments - if (args.isNotEmpty()) { - append("<") - append(args.joinToString { arg -> - when (arg.variance) { - KVariance.INVARIANT, null -> - arg.type?.let { renderType(it) } ?: "*" - - KVariance.IN -> - "in ${arg.type?.let { renderType(it) } ?: "Any?"}" - - KVariance.OUT -> - "out ${arg.type?.let { renderType(it) } ?: "Any?"}" - - } - }) - append(">") - } - - if (type.isMarkedNullable) { - append("?") - } - } - - return result - } - - private fun KVisibility?.isPublicApi(): Boolean { - return this == KVisibility.PUBLIC || this == KVisibility.PROTECTED - } -} - -@Serializable -data class GeneratedIndexData( - val version: String, - val kotlinVersion: String, - val generatedAt: Long = System.currentTimeMillis(), - val classes: Map = emptyMap(), - val topLevelFunctions: List = emptyList(), - val topLevelProperties: List = emptyList(), - val extensions: Map> = emptyMap(), - val typeAliases: List = emptyList() -) { - val totalCount: Int get() { - var count = classes.size + topLevelFunctions.size + topLevelProperties.size + typeAliases.size - classes.values.forEach { count += it.members.size } - extensions.values.forEach { count += it.size } - return count - } -} - -@Serializable -data class GeneratedClassEntry( - val fqName: String, - val kind: String, - val typeParams: List = emptyList(), - val supers: List = emptyList(), - val members: List = emptyList(), - val companions: List = emptyList(), - val nested: List = emptyList(), - val dep: Boolean = false, - val depMsg: String? = null -) - -@Serializable -data class GeneratedMemberEntry( - val name: String, - val kind: String, - val sig: String = "", - val params: List = emptyList(), - val ret: String? = null, - val vis: String = "PUBLIC", - val dep: Boolean = false -) - -@Serializable -data class GeneratedIndexEntry( - val name: String, - val fqName: String, - val kind: String, - val pkg: String, - val container: String? = null, - val sig: String = "", - val vis: String = "PUBLIC", - val typeParams: List = emptyList(), - val params: List = emptyList(), - val ret: String? = null, - val recv: String? = null, - val supers: List = emptyList(), - val file: String? = null, - val dep: Boolean = false, - val depMsg: String? = null -) - -@Serializable -data class GeneratedParamEntry( - val name: String, - val type: String, - val def: Boolean = false, - val vararg: Boolean = false -) diff --git a/settings.gradle.kts b/settings.gradle.kts index 6165b7a722..bf4dd33435 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -149,8 +149,6 @@ include( ":lsp:jvm-symbol-index", ":lsp:jvm-symbol-models", ":lsp:kotlin", - ":lsp:kotlin-core", - ":lsp:kotlin-stdlib-generator", ":lsp:xml", ":subprojects:aapt2-proto", ":subprojects:aaptcompiler", From fbab307c185e350f1d4a62b6d1ee06ad49a528f3 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 16 Apr 2026 00:54:15 +0530 Subject: [PATCH 49/58] fix: add basic test cases for indexing and keyword completions Signed-off-by: Akash Yadav --- lsp/indexing/build.gradle.kts | 3 + .../codeonthego/indexing/FilteredIndexTest.kt | 165 ++++++++ .../codeonthego/indexing/InMemoryIndexTest.kt | 249 ++++++++++++ .../codeonthego/indexing/MergedIndexTest.kt | 156 ++++++++ .../indexing/service/IndexRegistryTest.kt | 174 +++++++++ .../service/IndexingServiceManagerTest.kt | 119 ++++++ .../indexing/util/BackgroundIndexerTest.kt | 188 +++++++++ lsp/jvm-symbol-index/build.gradle.kts | 3 + .../indexing/jvm/JvmSymbolDescriptorTest.kt | 368 ++++++++++++++++++ .../indexing/jvm/JvmSymbolIndexTest.kt | 315 +++++++++++++++ .../codeonthego/indexing/jvm/JvmSymbolTest.kt | 262 +++++++++++++ .../indexing/jvm/KtFileMetadataTest.kt | 175 +++++++++ lsp/kotlin/build.gradle.kts | 1 + .../lsp/kotlin/utils/ContextKeywordsTest.kt | 175 +++++++++ .../utils/SymbolVisibilityCheckerTest.kt | 190 +++++++++ .../models/ToolingApiTestLauncherParams.kt | 4 +- 16 files changed, 2546 insertions(+), 1 deletion(-) create mode 100644 lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndexTest.kt create mode 100644 lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndexTest.kt create mode 100644 lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/MergedIndexTest.kt create mode 100644 lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/service/IndexRegistryTest.kt create mode 100644 lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManagerTest.kt create mode 100644 lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexerTest.kt create mode 100644 lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptorTest.kt create mode 100644 lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndexTest.kt create mode 100644 lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolTest.kt create mode 100644 lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataTest.kt create mode 100644 lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywordsTest.kt create mode 100644 lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityCheckerTest.kt diff --git a/lsp/indexing/build.gradle.kts b/lsp/indexing/build.gradle.kts index b81c2d47b3..4ccc7230dd 100644 --- a/lsp/indexing/build.gradle.kts +++ b/lsp/indexing/build.gradle.kts @@ -16,4 +16,7 @@ dependencies { api(libs.kotlinx.coroutines.core) api(projects.logger) + + testImplementation(projects.testing.unit) + testImplementation(libs.tests.kotlinx.coroutines) } diff --git a/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndexTest.kt b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndexTest.kt new file mode 100644 index 0000000000..81168d8d74 --- /dev/null +++ b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndexTest.kt @@ -0,0 +1,165 @@ +package org.appdevforall.codeonthego.indexing + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.appdevforall.codeonthego.indexing.api.IndexDescriptor +import org.appdevforall.codeonthego.indexing.api.IndexField +import org.appdevforall.codeonthego.indexing.api.IndexQuery +import org.appdevforall.codeonthego.indexing.api.Indexable +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class FilteredIndexTest { + + data class Entry( + override val key: String, + override val sourceId: String, + val value: String, + ) : Indexable + + private val descriptor = object : IndexDescriptor { + override val name = "test_filtered" + override val fields = listOf(IndexField("value")) + + override fun fieldValues(entry: Entry) = mapOf("value" to entry.value) + + override fun serialize(entry: Entry) = + "${entry.key}|${entry.sourceId}|${entry.value}".toByteArray() + + override fun deserialize(bytes: ByteArray): Entry { + val parts = String(bytes).split("|") + return Entry(parts[0], parts[1], parts[2]) + } + } + + private suspend fun setupBackingAndFiltered(): Pair, FilteredIndex> { + val backing = InMemoryIndex(descriptor) + backing.insert(Entry("k1", "src1", "val1")) + backing.insert(Entry("k2", "src1", "val2")) + backing.insert(Entry("k3", "src2", "val3")) + return backing to FilteredIndex(backing) + } + + @Test + fun `query returns nothing when no sources active`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + assertThat(filtered.query(IndexQuery.ALL).toList()).isEmpty() + } + + @Test + fun `query returns entries for active source`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + val keys = filtered.query(IndexQuery.ALL).map { it.key }.toList() + assertThat(keys).containsExactly("k1", "k2") + } + + @Test + fun `deactivateSource hides entries from that source`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + filtered.activateSource("src2") + filtered.deactivateSource("src1") + + val keys = filtered.query(IndexQuery.ALL).map { it.key }.toList() + assertThat(keys).containsExactly("k3") + } + + @Test + fun `setActiveSources replaces entire active set`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + filtered.setActiveSources(setOf("src2")) + + val keys = filtered.query(IndexQuery.ALL).map { it.key }.toList() + assertThat(keys).containsExactly("k3") + } + + @Test + fun `get returns null when source is inactive`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + assertThat(filtered.get("k1")).isNull() + } + + @Test + fun `get returns entry when source is active`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + assertThat(filtered.get("k1")).isNotNull() + assertThat(filtered.get("k1")!!.key).isEqualTo("k1") + } + + @Test + fun `get returns null for nonexistent key even with active source`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + assertThat(filtered.get("missing")).isNull() + } + + @Test + fun `containsSource returns false when source inactive`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + assertThat(filtered.containsSource("src1")).isFalse() + } + + @Test + fun `containsSource returns true only when source is active AND in backing`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + assertThat(filtered.containsSource("src1")).isTrue() + assertThat(filtered.containsSource("src999")).isFalse() + } + + @Test + fun `isCached returns true if source exists in backing regardless of active state`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + assertThat(filtered.isCached("src1")).isTrue() + assertThat(filtered.isCached("src999")).isFalse() + } + + @Test + fun `activeSources returns current active set`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + filtered.activateSource("src2") + assertThat(filtered.activeSources()).containsExactly("src1", "src2") + } + + @Test + fun `isActive reflects current state`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + assertThat(filtered.isActive("src1")).isFalse() + filtered.activateSource("src1") + assertThat(filtered.isActive("src1")).isTrue() + filtered.deactivateSource("src1") + assertThat(filtered.isActive("src1")).isFalse() + } + + @Test + fun `explicit sourceId query returns empty for inactive source`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + // src1 has entries in backing but is not active + val results = filtered.query(IndexQuery.bySource("src1")).toList() + assertThat(results).isEmpty() + } + + @Test + fun `all sources active returns all entries from backing`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + filtered.activateSource("src2") + + val results = filtered.query(IndexQuery.ALL).toList() + assertThat(results).hasSize(3) + } + + @Test + fun `close clears active sources`() = runTest { + val (_, filtered) = setupBackingAndFiltered() + filtered.activateSource("src1") + filtered.close() + assertThat(filtered.activeSources()).isEmpty() + } +} diff --git a/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndexTest.kt b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndexTest.kt new file mode 100644 index 0000000000..45cff41032 --- /dev/null +++ b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndexTest.kt @@ -0,0 +1,249 @@ +package org.appdevforall.codeonthego.indexing + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.appdevforall.codeonthego.indexing.api.IndexDescriptor +import org.appdevforall.codeonthego.indexing.api.IndexField +import org.appdevforall.codeonthego.indexing.api.IndexQuery +import org.appdevforall.codeonthego.indexing.api.Indexable +import org.appdevforall.codeonthego.indexing.api.indexQuery +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class InMemoryIndexTest { + + data class TestEntry( + override val key: String, + override val sourceId: String, + val name: String, + val category: String? = null, + ) : Indexable + + private val descriptor = object : IndexDescriptor { + override val name = "test" + override val fields = listOf( + IndexField("name", prefixSearchable = true), + IndexField("category"), + ) + + override fun fieldValues(entry: TestEntry) = mapOf( + "name" to entry.name, + "category" to entry.category, + ) + + override fun serialize(entry: TestEntry) = + "${entry.key}|${entry.sourceId}|${entry.name}|${entry.category}".toByteArray() + + override fun deserialize(bytes: ByteArray): TestEntry { + val parts = String(bytes).split("|") + return TestEntry(parts[0], parts[1], parts[2], parts[3].takeIf { it != "null" }) + } + } + + private fun makeIndex() = InMemoryIndex(descriptor) + + private fun entry(key: String, sourceId: String, name: String, category: String? = null) = + TestEntry(key, sourceId, name, category) + + @Test + fun `insert and get by key`() = runTest { + val index = makeIndex() + val e = entry("k1", "src1", "Foo") + index.insert(e) + assertThat(index.get("k1")).isEqualTo(e) + assertThat(index.get("missing")).isNull() + } + + @Test + fun `query returns all entries when no predicates`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha")) + index.insert(entry("k2", "src1", "Beta")) + index.insert(entry("k3", "src2", "Gamma")) + + val results = index.query(IndexQuery.ALL).toList() + assertThat(results).hasSize(3) + } + + @Test + fun `exact match on field`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha", "lib")) + index.insert(entry("k2", "src1", "Beta", "app")) + index.insert(entry("k3", "src2", "Gamma", "lib")) + + val results = index.query(indexQuery { eq("category", "lib") }).toList() + assertThat(results.map { it.key }).containsExactly("k1", "k3") + } + + @Test + fun `prefix match on field`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "ArrayList")) + index.insert(entry("k2", "src1", "ArrayDeque")) + index.insert(entry("k3", "src1", "String")) + + val results = index.query(indexQuery { prefix("name", "Array") }).toList() + assertThat(results.map { it.key }).containsExactlyElementsIn(listOf("k1", "k2")) + } + + @Test + fun `prefix match is case-insensitive`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "ArrayList")) + index.insert(entry("k2", "src1", "ARRAYDEQUE")) + + val results = index.query(indexQuery { prefix("name", "array") }).toList() + assertThat(results).hasSize(2) + } + + @Test + fun `containsSource returns true for indexed source`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Foo")) + assertThat(index.containsSource("src1")).isTrue() + assertThat(index.containsSource("src999")).isFalse() + } + + @Test + fun `removeBySource removes all entries from that source`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha")) + index.insert(entry("k2", "src1", "Beta")) + index.insert(entry("k3", "src2", "Gamma")) + + index.removeBySource("src1") + + assertThat(index.get("k1")).isNull() + assertThat(index.get("k2")).isNull() + assertThat(index.get("k3")).isNotNull() + assertThat(index.containsSource("src1")).isFalse() + assertThat(index.size).isEqualTo(1) + } + + @Test + fun `clear removes all entries`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha")) + index.insert(entry("k2", "src2", "Beta")) + + index.clear() + + assertThat(index.size).isEqualTo(0) + assertThat(index.sourceCount).isEqualTo(0) + } + + @Test + fun `insert replaces existing entry with same key`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Original")) + index.insert(entry("k1", "src1", "Updated")) + + assertThat(index.get("k1")!!.name).isEqualTo("Updated") + assertThat(index.size).isEqualTo(1) + } + + @Test + fun `query by key returns single match`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha")) + index.insert(entry("k2", "src1", "Beta")) + + val results = index.query(IndexQuery.byKey("k1")).toList() + assertThat(results).hasSize(1) + assertThat(results[0].key).isEqualTo("k1") + } + + @Test + fun `query by source returns entries for that source`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha")) + index.insert(entry("k2", "src1", "Beta")) + index.insert(entry("k3", "src2", "Gamma")) + + val results = index.query(IndexQuery.bySource("src1")).toList() + assertThat(results.map { it.key }).containsExactly("k1", "k2") + } + + @Test + fun `query respects limit`() = runTest { + val index = makeIndex() + repeat(10) { i -> index.insert(entry("k$i", "src1", "Name$i")) } + + val results = index.query(IndexQuery(limit = 3)).toList() + assertThat(results).hasSize(3) + } + + @Test + fun `presence filter - field exists`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha", "lib")) + index.insert(entry("k2", "src1", "Beta")) // no category + + val results = index.query(indexQuery { exists("category") }).toList() + assertThat(results.map { it.key }).containsExactly("k1") + } + + @Test + fun `presence filter - field absent`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha", "lib")) + index.insert(entry("k2", "src1", "Beta")) + + val results = index.query(indexQuery { notExists("category") }).toList() + assertThat(results.map { it.key }).containsExactly("k2") + } + + @Test + fun `distinctValues returns unique field values`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Alpha", "lib")) + index.insert(entry("k2", "src1", "Beta", "lib")) + index.insert(entry("k3", "src2", "Gamma", "app")) + + val values = index.distinctValues("category").toSet() + assertThat(values).containsExactly("lib", "app") + } + + @Test + fun `multi-field combined query`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "ArrayList", "java")) + index.insert(entry("k2", "src1", "ArrayDeque", "java")) + index.insert(entry("k3", "src2", "ArrayBlockingQueue", "concurrent")) + + val results = index.query(indexQuery { + eq("category", "java") + prefix("name", "Array") + }).toList() + assertThat(results.map { it.key }).containsExactlyElementsIn(listOf("k1", "k2")) + } + + @Test + fun `insertAll inserts multiple entries`() = runTest { + val index = makeIndex() + val entries = (1..5).map { i -> entry("k$i", "src1", "Name$i") } + index.insertAll(entries.asSequence()) + + assertThat(index.size).isEqualTo(5) + } + + @Test + fun `query with unknown field returns empty sequence`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Foo")) + + val results = index.query(indexQuery { eq("nonexistentField", "value") }).toList() + assertThat(results).isEmpty() + } + + @Test + fun `removeBySource is no-op for unknown source`() = runTest { + val index = makeIndex() + index.insert(entry("k1", "src1", "Foo")) + index.removeBySource("unknownSource") + assertThat(index.size).isEqualTo(1) + } +} diff --git a/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/MergedIndexTest.kt b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/MergedIndexTest.kt new file mode 100644 index 0000000000..d41c8ed03c --- /dev/null +++ b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/MergedIndexTest.kt @@ -0,0 +1,156 @@ +package org.appdevforall.codeonthego.indexing + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.appdevforall.codeonthego.indexing.api.IndexDescriptor +import org.appdevforall.codeonthego.indexing.api.IndexField +import org.appdevforall.codeonthego.indexing.api.IndexQuery +import org.appdevforall.codeonthego.indexing.api.Indexable +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class MergedIndexTest { + + data class Entry( + override val key: String, + override val sourceId: String, + val value: String, + ) : Indexable + + private val descriptor = object : IndexDescriptor { + override val name = "test_merged" + override val fields = listOf(IndexField("value")) + + override fun fieldValues(entry: Entry) = mapOf("value" to entry.value) + + override fun serialize(entry: Entry) = + "${entry.key}|${entry.sourceId}|${entry.value}".toByteArray() + + override fun deserialize(bytes: ByteArray): Entry { + val parts = String(bytes).split("|") + return Entry(parts[0], parts[1], parts[2]) + } + } + + private fun makeIndex() = InMemoryIndex(descriptor) + + @Test + fun `merges results from multiple indexes`() = runTest { + val idx1 = makeIndex().also { it.insert(Entry("k1", "s1", "v1")) } + val idx2 = makeIndex().also { it.insert(Entry("k2", "s2", "v2")) } + val merged = MergedIndex(idx1, idx2) + + val keys = merged.query(IndexQuery.ALL).map { it.key }.toList() + assertThat(keys).containsExactly("k1", "k2") + } + + @Test + fun `deduplicates by key - first index wins`() = runTest { + val idx1 = makeIndex().also { it.insert(Entry("k1", "s1", "from-index1")) } + val idx2 = makeIndex().also { it.insert(Entry("k1", "s2", "from-index2")) } + val merged = MergedIndex(idx1, idx2) + + val results = merged.query(IndexQuery.ALL).toList() + assertThat(results).hasSize(1) + assertThat(results[0].value).isEqualTo("from-index1") + } + + @Test + fun `respects limit across indexes`() = runTest { + val idx1 = makeIndex().also { e -> + (1..5).forEach { i -> e.insert(Entry("k$i", "s1", "v$i")) } + } + val idx2 = makeIndex().also { e -> + (6..10).forEach { i -> e.insert(Entry("k$i", "s2", "v$i")) } + } + val merged = MergedIndex(idx1, idx2) + + val results = merged.query(IndexQuery(limit = 3)).toList() + assertThat(results).hasSize(3) + } + + @Test + fun `get returns first match in priority order`() = runTest { + val idx1 = makeIndex().also { it.insert(Entry("k1", "s1", "primary")) } + val idx2 = makeIndex().also { it.insert(Entry("k1", "s2", "secondary")) } + val merged = MergedIndex(idx1, idx2) + + assertThat(merged.get("k1")!!.value).isEqualTo("primary") + } + + @Test + fun `get returns from second index when not in first`() = runTest { + val idx1 = makeIndex() + val idx2 = makeIndex().also { it.insert(Entry("k2", "s2", "second")) } + val merged = MergedIndex(idx1, idx2) + + assertThat(merged.get("k2")!!.value).isEqualTo("second") + } + + @Test + fun `get returns null if key not in any index`() = runTest { + val merged = MergedIndex(makeIndex()) + assertThat(merged.get("missing")).isNull() + } + + @Test + fun `containsSource checks all indexes`() = runTest { + val idx1 = makeIndex().also { it.insert(Entry("k1", "src1", "v1")) } + val idx2 = makeIndex().also { it.insert(Entry("k2", "src2", "v2")) } + val merged = MergedIndex(idx1, idx2) + + assertThat(merged.containsSource("src1")).isTrue() + assertThat(merged.containsSource("src2")).isTrue() + assertThat(merged.containsSource("src999")).isFalse() + } + + @Test + fun `distinctValues deduplicates across indexes`() = runTest { + val idx1 = makeIndex().also { e -> + e.insert(Entry("k1", "s1", "alpha")) + e.insert(Entry("k2", "s1", "beta")) + } + val idx2 = makeIndex().also { e -> + e.insert(Entry("k3", "s2", "beta")) // duplicate + e.insert(Entry("k4", "s2", "gamma")) + } + val merged = MergedIndex(idx1, idx2) + + val values = merged.distinctValues("value").toSet() + assertThat(values).containsExactly("alpha", "beta", "gamma") + } + + @Test + fun `empty merged index returns empty results`() = runTest { + val merged = MergedIndex(emptyList()) + + assertThat(merged.query(IndexQuery.ALL).toList()).isEmpty() + assertThat(merged.get("k1")).isNull() + assertThat(merged.containsSource("s1")).isFalse() + assertThat(merged.distinctValues("value").toList()).isEmpty() + } + + @Test + fun `single index merged works identically to unmerged`() = runTest { + val idx = makeIndex().also { + it.insert(Entry("k1", "s1", "val1")) + it.insert(Entry("k2", "s1", "val2")) + } + val merged = MergedIndex(idx) + + assertThat(merged.query(IndexQuery.ALL).toList()).hasSize(2) + assertThat(merged.get("k1")).isNotNull() + assertThat(merged.containsSource("s1")).isTrue() + } + + @Test + fun `vararg constructor builds merged index`() = runTest { + val idx1 = makeIndex().also { it.insert(Entry("k1", "s1", "a")) } + val idx2 = makeIndex().also { it.insert(Entry("k2", "s2", "b")) } + val merged = MergedIndex(idx1, idx2) + + assertThat(merged.query(IndexQuery.ALL).toList()).hasSize(2) + } +} diff --git a/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/service/IndexRegistryTest.kt b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/service/IndexRegistryTest.kt new file mode 100644 index 0000000000..6181fcddbc --- /dev/null +++ b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/service/IndexRegistryTest.kt @@ -0,0 +1,174 @@ +package org.appdevforall.codeonthego.indexing.service + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import java.io.Closeable + +@RunWith(JUnit4::class) +class IndexRegistryTest { + + private val keyA = IndexKey("index-a") + private val keyB = IndexKey("index-b") + + @Test + fun `get returns null before registration`() { + val registry = IndexRegistry() + assertThat(registry.get(keyA)).isNull() + } + + @Test + fun `register and get return same instance`() { + val registry = IndexRegistry() + registry.register(keyA, "hello") + assertThat(registry.get(keyA)).isEqualTo("hello") + } + + @Test + fun `require throws if not registered`() { + val registry = IndexRegistry() + try { + registry.require(keyA) + error("expected exception not thrown") + } catch (e: IllegalStateException) { + assertThat(e.message).contains("index-a") + } + } + + @Test + fun `require returns value when registered`() { + val registry = IndexRegistry() + registry.register(keyA, "world") + assertThat(registry.require(keyA)).isEqualTo("world") + } + + @Test + fun `isRegistered reflects current state`() { + val registry = IndexRegistry() + assertThat(registry.isRegistered(keyA)).isFalse() + registry.register(keyA, "x") + assertThat(registry.isRegistered(keyA)).isTrue() + } + + @Test + fun `registeredKeys returns all registered key names`() { + val registry = IndexRegistry() + registry.register(keyA, "x") + registry.register(keyB, 42) + assertThat(registry.registeredKeys()).containsExactly("index-a", "index-b") + } + + @Test + fun `ifAvailable returns null when not registered`() { + val registry = IndexRegistry() + val result = registry.ifAvailable(keyA) { it.length } + assertThat(result).isNull() + } + + @Test + fun `ifAvailable invokes block when registered`() { + val registry = IndexRegistry() + registry.register(keyA, "hello") + val result = registry.ifAvailable(keyA) { it.length } + assertThat(result).isEqualTo(5) + } + + @Test + fun `re-registering replaces old value`() { + val registry = IndexRegistry() + registry.register(keyA, "first") + registry.register(keyA, "second") + assertThat(registry.get(keyA)).isEqualTo("second") + } + + @Test + fun `onAvailable notifies immediately if already registered`() { + val registry = IndexRegistry() + registry.register(keyA, "present") + + var received: String? = null + registry.onAvailable(keyA) { received = it } + + assertThat(received).isEqualTo("present") + } + + @Test + fun `onAvailable notifies after subsequent registration`() { + val registry = IndexRegistry() + + var received: String? = null + registry.onAvailable(keyA) { received = it } + assertThat(received).isNull() + + registry.register(keyA, "later") + assertThat(received).isEqualTo("later") + } + + @Test + fun `onAvailable listener is called on re-registration`() { + val registry = IndexRegistry() + registry.register(keyA, "first") + + val received = mutableListOf() + registry.onAvailable(keyA) { received.add(it) } + + registry.register(keyA, "second") + + assertThat(received).containsExactly("first", "second") + } + + @Test + fun `unregister returns old value`() { + val registry = IndexRegistry() + registry.register(keyA, "value") + val old = registry.unregister(keyA) + assertThat(old).isEqualTo("value") + assertThat(registry.get(keyA)).isNull() + } + + @Test + fun `unregister returns null if not registered`() { + val registry = IndexRegistry() + assertThat(registry.unregister(keyA)).isNull() + } + + @Test + fun `close clears all indexes and listeners`() { + val registry = IndexRegistry() + registry.register(keyA, "x") + registry.register(keyB, 42) + registry.close() + assertThat(registry.registeredKeys()).isEmpty() + } + + @Test + fun `close calls close on Closeable indexes`() { + val registry = IndexRegistry() + val keyC = IndexKey("index-c") + val closeable = CloseableValue() + registry.register(keyC, closeable) + registry.close() + assertThat(closeable.closed).isTrue() + } + + @Test + fun `multiple keys coexist independently`() { + val registry = IndexRegistry() + registry.register(keyA, "text") + registry.register(keyB, 99) + assertThat(registry.get(keyA)).isEqualTo("text") + assertThat(registry.get(keyB)).isEqualTo(99) + } + + @Test + fun `registeredKeys returns empty set when nothing registered`() { + val registry = IndexRegistry() + assertThat(registry.registeredKeys()).isEmpty() + } + + class CloseableValue : Closeable { + var closed = false + override fun close() { closed = true } + } +} diff --git a/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManagerTest.kt b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManagerTest.kt new file mode 100644 index 0000000000..02d1cf6ef4 --- /dev/null +++ b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManagerTest.kt @@ -0,0 +1,119 @@ +package org.appdevforall.codeonthego.indexing.service + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class IndexingServiceManagerTest { + + class TestService(override val id: String) : IndexingService { + override val providedKeys = emptyList>() + var initialized = false + var buildCompleted = false + var closed = false + + override suspend fun initialize(registry: IndexRegistry) { + initialized = true + } + + override suspend fun onBuildCompleted() { + buildCompleted = true + } + + override fun close() { + closed = true + } + } + + @Test + fun `register adds service retrievable by id`() { + val manager = IndexingServiceManager() + val svc = TestService("svc-a") + manager.register(svc) + assertThat(manager.getService("svc-a")).isSameInstanceAs(svc) + manager.close() + } + + @Test + fun `duplicate registration is silently ignored`() { + val manager = IndexingServiceManager() + val svc1 = TestService("svc-a") + val svc2 = TestService("svc-a") + manager.register(svc1) + manager.register(svc2) + assertThat(manager.getService("svc-a")).isSameInstanceAs(svc1) + manager.close() + } + + @Test + fun `allServices returns all registered services`() { + val manager = IndexingServiceManager() + manager.register(TestService("a")) + manager.register(TestService("b")) + manager.register(TestService("c")) + assertThat(manager.allServices()).hasSize(3) + manager.close() + } + + @Test + fun `getService returns null for unregistered id`() { + val manager = IndexingServiceManager() + assertThat(manager.getService("unknown")).isNull() + manager.close() + } + + @Test + fun `close calls close on each registered service`() { + val manager = IndexingServiceManager() + val svc1 = TestService("a") + val svc2 = TestService("b") + manager.register(svc1) + manager.register(svc2) + manager.close() + assertThat(svc1.closed).isTrue() + assertThat(svc2.closed).isTrue() + } + + @Test + fun `close clears services list`() { + val manager = IndexingServiceManager() + manager.register(TestService("a")) + manager.close() + assertThat(manager.allServices()).isEmpty() + } + + @Test + fun `registry is accessible and starts empty`() { + val manager = IndexingServiceManager() + assertThat(manager.registry).isNotNull() + assertThat(manager.registry.registeredKeys()).isEmpty() + manager.close() + } + + @Test + fun `onBuildCompleted before initialization does not throw`() { + val manager = IndexingServiceManager() + val svc = TestService("a") + manager.register(svc) + // Should be a no-op and not throw + manager.onBuildCompleted() + manager.close() + } + + @Test + fun `onSourceChanged before initialization is a no-op`() { + val manager = IndexingServiceManager() + manager.register(TestService("a")) + manager.onSourceChanged() + manager.close() + } + + @Test + fun `close after close does not throw`() { + val manager = IndexingServiceManager() + manager.close() + manager.close() // second close should be safe + } +} diff --git a/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexerTest.kt b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexerTest.kt new file mode 100644 index 0000000000..0e3d96380f --- /dev/null +++ b/lsp/indexing/src/test/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexerTest.kt @@ -0,0 +1,188 @@ +package org.appdevforall.codeonthego.indexing.util + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.appdevforall.codeonthego.indexing.InMemoryIndex +import org.appdevforall.codeonthego.indexing.api.IndexDescriptor +import org.appdevforall.codeonthego.indexing.api.IndexField +import org.appdevforall.codeonthego.indexing.api.Indexable +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class BackgroundIndexerTest { + + data class Entry( + override val key: String, + override val sourceId: String, + val value: String, + ) : Indexable + + private val descriptor = object : IndexDescriptor { + override val name = "bg_test" + override val fields = listOf(IndexField("value")) + + override fun fieldValues(entry: Entry) = mapOf("value" to entry.value) + + override fun serialize(entry: Entry) = + "${entry.key}|${entry.sourceId}|${entry.value}".toByteArray() + + override fun deserialize(bytes: ByteArray): Entry { + val parts = String(bytes).split("|") + return Entry(parts[0], parts[1], parts[2]) + } + } + + private fun makeIndexAndIndexer(): Pair, BackgroundIndexer> { + val index = InMemoryIndex(descriptor) + return index to BackgroundIndexer(index) + } + + @Test + fun `indexSource inserts all provided entries`() = runTest { + val (index, indexer) = makeIndexAndIndexer() + + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf( + Entry("k1", sourceId, "v1"), + Entry("k2", sourceId, "v2"), + ) + }.join() + + assertThat(index.size).isEqualTo(2) + assertThat(index.get("k1")).isNotNull() + assertThat(index.get("k2")).isNotNull() + } + + @Test + fun `skipIfExists=true skips re-indexing of existing source`() = runTest { + val (index, indexer) = makeIndexAndIndexer() + + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf(Entry("k1", sourceId, "original")) + }.join() + + indexer.indexSource("src1", skipIfExists = true) { sourceId -> + sequenceOf(Entry("k1", sourceId, "updated")) + }.join() + + assertThat(index.get("k1")!!.value).isEqualTo("original") + } + + @Test + fun `skipIfExists=false forces re-indexing`() = runTest { + val (index, indexer) = makeIndexAndIndexer() + + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf(Entry("k1", sourceId, "original")) + }.join() + + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf(Entry("k1", sourceId, "updated")) + }.join() + + assertThat(index.get("k1")!!.value).isEqualTo("updated") + } + + @Test + fun `progressListener receives Started and Completed events`() = runTest { + val (_, indexer) = makeIndexAndIndexer() + val events = mutableListOf() + indexer.progressListener = IndexingProgressListener { _, event -> events.add(event) } + + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf(Entry("k1", sourceId, "v1")) + }.join() + + assertThat(events).contains(IndexingEvent.Started) + val completed = events.filterIsInstance() + assertThat(completed).hasSize(1) + assertThat(completed.first().totalIndexed).isEqualTo(1) + } + + @Test + fun `progressListener receives Skipped when source already indexed`() = runTest { + val (_, indexer) = makeIndexAndIndexer() + + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf(Entry("k1", sourceId, "v1")) + }.join() + + val events = mutableListOf() + indexer.progressListener = IndexingProgressListener { _, event -> events.add(event) } + + indexer.indexSource("src1", skipIfExists = true) { _ -> + emptySequence() + }.join() + + assertThat(events).contains(IndexingEvent.Skipped) + } + + @Test + fun `awaitAll waits for all in-flight jobs`() = runTest { + val (index, indexer) = makeIndexAndIndexer() + + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf(Entry("k1", sourceId, "v1")) + } + indexer.indexSource("src2", skipIfExists = false) { sourceId -> + sequenceOf(Entry("k2", sourceId, "v2")) + } + + indexer.awaitAll() + + assertThat(index.size).isEqualTo(2) + } + + @Test + fun `indexSources indexes all provided sources`() = runTest { + val (index, indexer) = makeIndexAndIndexer() + val sources = listOf("jar1", "jar2", "jar3") + + val jobs = indexer.indexSources(sources, skipIfExists = false) { sourceId -> + sourceId to sequenceOf(Entry("key-$sourceId", sourceId, "val")) + } + jobs.forEach { it.join() } + + assertThat(index.size).isEqualTo(3) + assertThat(index.containsSource("jar1")).isTrue() + assertThat(index.containsSource("jar2")).isTrue() + assertThat(index.containsSource("jar3")).isTrue() + } + + @Test + fun `activeJobCount reflects in-flight jobs`() = runTest { + val (_, indexer) = makeIndexAndIndexer() + assertThat(indexer.activeJobCount).isEqualTo(0) + } + + @Test + fun `close cancels all active jobs`() = runTest { + val (_, indexer) = makeIndexAndIndexer() + indexer.close() + assertThat(indexer.activeJobCount).isEqualTo(0) + } + + @Test + fun `indexSource removes stale entries before re-indexing`() = runTest { + val (index, indexer) = makeIndexAndIndexer() + + // Index with two entries + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf( + Entry("k1", sourceId, "old1"), + Entry("k2", sourceId, "old2"), + ) + }.join() + + // Re-index with only one entry + indexer.indexSource("src1", skipIfExists = false) { sourceId -> + sequenceOf(Entry("k1", sourceId, "new1")) + }.join() + + assertThat(index.size).isEqualTo(1) + assertThat(index.get("k1")!!.value).isEqualTo("new1") + assertThat(index.get("k2")).isNull() + } +} diff --git a/lsp/jvm-symbol-index/build.gradle.kts b/lsp/jvm-symbol-index/build.gradle.kts index 959f2264be..a26497c9c2 100644 --- a/lsp/jvm-symbol-index/build.gradle.kts +++ b/lsp/jvm-symbol-index/build.gradle.kts @@ -21,4 +21,7 @@ dependencies { api(projects.lsp.jvmSymbolModels) api(projects.subprojects.kotlinAnalysisApi) api(projects.subprojects.projects) + + testImplementation(projects.testing.unit) + testImplementation(libs.tests.kotlinx.coroutines) } diff --git a/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptorTest.kt b/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptorTest.kt new file mode 100644 index 0000000000..de10f07d10 --- /dev/null +++ b/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolDescriptorTest.kt @@ -0,0 +1,368 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class JvmSymbolDescriptorTest { + + private fun classSymbol( + name: String = "com/example/Foo", + shortName: String = "Foo", + pkg: String = "com.example", + sourceId: String = "foo.jar", + kind: JvmSymbolKind = JvmSymbolKind.CLASS, + language: JvmSourceLanguage = JvmSourceLanguage.KOTLIN, + visibility: JvmVisibility = JvmVisibility.PUBLIC, + kotlin: KotlinClassInfo? = null, + containingClass: String = "", + ) = JvmSymbol( + key = name, + sourceId = sourceId, + name = name, + shortName = shortName, + packageName = pkg, + kind = kind, + language = language, + visibility = visibility, + data = JvmClassInfo(containingClassName = containingClass, kotlin = kotlin), + ) + + private fun roundtrip(symbol: JvmSymbol): JvmSymbol = + JvmSymbolDescriptor.deserialize(JvmSymbolDescriptor.serialize(symbol)) + + @Test + fun `roundtrip preserves all core fields for class`() { + val symbol = classSymbol( + name = "com/example/MyClass", + shortName = "MyClass", + pkg = "com.example", + sourceId = "mylib.jar", + visibility = JvmVisibility.INTERNAL, + language = JvmSourceLanguage.JAVA, + ) + val restored = roundtrip(symbol) + assertThat(restored.name).isEqualTo(symbol.name) + assertThat(restored.shortName).isEqualTo(symbol.shortName) + assertThat(restored.packageName).isEqualTo(symbol.packageName) + assertThat(restored.sourceId).isEqualTo(symbol.sourceId) + assertThat(restored.kind).isEqualTo(symbol.kind) + assertThat(restored.language).isEqualTo(symbol.language) + assertThat(restored.visibility).isEqualTo(symbol.visibility) + } + + @Test + fun `roundtrip preserves kotlin class metadata`() { + val symbol = classSymbol( + kind = JvmSymbolKind.DATA_CLASS, + kotlin = KotlinClassInfo( + isData = true, + companionObjectName = "Companion", + sealedSubclasses = listOf("com/example/Foo\$Sub1", "com/example/Foo\$Sub2"), + isSealed = false, + ), + ) + val restored = roundtrip(symbol) + val kt = (restored.data as JvmClassInfo).kotlin!! + assertThat(kt.isData).isTrue() + assertThat(kt.companionObjectName).isEqualTo("Companion") + assertThat(kt.sealedSubclasses).containsExactly("com/example/Foo\$Sub1", "com/example/Foo\$Sub2") + } + + @Test + fun `roundtrip for sealed class with subclasses`() { + val symbol = classSymbol( + kind = JvmSymbolKind.SEALED_CLASS, + kotlin = KotlinClassInfo( + isSealed = true, + sealedSubclasses = listOf("A", "B", "C"), + ), + ) + val kt = (roundtrip(symbol).data as JvmClassInfo).kotlin!! + assertThat(kt.isSealed).isTrue() + assertThat(kt.sealedSubclasses).containsExactly("A", "B", "C") + } + + @Test + fun `roundtrip for class without kotlin metadata`() { + val symbol = classSymbol(language = JvmSourceLanguage.JAVA) + val restored = roundtrip(symbol) + assertThat((restored.data as JvmClassInfo).kotlin).isNull() + } + + @Test + fun `roundtrip preserves function with parameters`() { + val params = listOf( + JvmParameterInfo("x", "java/lang/String", "String"), + JvmParameterInfo("y", "I", "Int", hasDefaultValue = true), + ) + val symbol = JvmSymbol( + key = "com/example/Foo.bar(java/lang/String,I)", + sourceId = "foo.jar", + name = "com/example/Foo.bar", + shortName = "bar", + packageName = "com.example", + kind = JvmSymbolKind.FUNCTION, + language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo( + containingClassName = "com/example/Foo", + returnTypeName = "kotlin/String", + returnTypeDisplayName = "String", + parameterCount = 2, + parameters = params, + signatureDisplay = "fun bar(x: String, y: Int = 0): String", + typeParameters = listOf("T"), + isStatic = false, + ), + ) + val restored = roundtrip(symbol) + val data = restored.data as JvmFunctionInfo + assertThat(data.parameters).hasSize(2) + assertThat(data.parameters[0].name).isEqualTo("x") + assertThat(data.parameters[0].typeName).isEqualTo("java/lang/String") + assertThat(data.parameters[1].hasDefaultValue).isTrue() + assertThat(data.returnTypeDisplayName).isEqualTo("String") + assertThat(data.signatureDisplay).isEqualTo("fun bar(x: String, y: Int = 0): String") + assertThat(data.typeParameters).containsExactly("T") + } + + @Test + fun `roundtrip preserves kotlin function metadata`() { + val symbol = JvmSymbol( + key = "com/example/exFun()", + sourceId = "foo.jar", + name = "com/example/exFun", + shortName = "exFun", + packageName = "com.example", + kind = JvmSymbolKind.EXTENSION_FUNCTION, + language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo( + kotlin = KotlinFunctionInfo( + receiverTypeName = "kotlin/collections/List", + receiverTypeDisplayName = "List<*>", + isSuspend = true, + isInline = true, + isInfix = false, + isOperator = false, + ), + ), + ) + val kt = (roundtrip(symbol).data as JvmFunctionInfo).kotlin!! + assertThat(kt.receiverTypeName).isEqualTo("kotlin/collections/List") + assertThat(kt.receiverTypeDisplayName).isEqualTo("List<*>") + assertThat(kt.isSuspend).isTrue() + assertThat(kt.isInline).isTrue() + } + + @Test + fun `roundtrip preserves vararg parameter flag`() { + val symbol = JvmSymbol( + key = "f(I)", + sourceId = "s", + name = "f", + shortName = "f", + packageName = "p", + kind = JvmSymbolKind.FUNCTION, + language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo( + parameters = listOf( + JvmParameterInfo("args", "[I", "IntArray", isVararg = true), + ), + ), + ) + val restored = roundtrip(symbol) + val param = (restored.data as JvmFunctionInfo).parameters.first() + assertThat(param.isVararg).isTrue() + } + + @Test + fun `roundtrip preserves field with kotlin property metadata`() { + val symbol = JvmSymbol( + key = "com/example/Foo.count", + sourceId = "foo.jar", + name = "com/example/Foo.count", + shortName = "count", + packageName = "com.example", + kind = JvmSymbolKind.PROPERTY, + language = JvmSourceLanguage.KOTLIN, + data = JvmFieldInfo( + containingClassName = "com/example/Foo", + typeName = "I", + typeDisplayName = "Int", + isFinal = true, + kotlin = KotlinPropertyInfo( + isConst = true, + hasGetter = true, + hasSetter = false, + isTypeNullable = false, + ), + ), + ) + val data = roundtrip(symbol).data as JvmFieldInfo + assertThat(data.typeDisplayName).isEqualTo("Int") + assertThat(data.isFinal).isTrue() + val kotlin = data.kotlin!! + assertThat(kotlin.isConst).isTrue() + assertThat(kotlin.hasGetter).isTrue() + assertThat(kotlin.hasSetter).isFalse() + } + + @Test + fun `roundtrip preserves enum entry`() { + val symbol = JvmSymbol( + key = "com/example/Status.ACTIVE", + sourceId = "foo.jar", + name = "com/example/Status.ACTIVE", + shortName = "ACTIVE", + packageName = "com.example", + kind = JvmSymbolKind.ENUM_ENTRY, + language = JvmSourceLanguage.KOTLIN, + data = JvmEnumEntryInfo(containingClassName = "com/example/Status", ordinal = 2), + ) + val data = roundtrip(symbol).data as JvmEnumEntryInfo + assertThat(data.ordinal).isEqualTo(2) + assertThat(data.containingClassName).isEqualTo("com/example/Status") + } + + @Test + fun `roundtrip preserves type alias`() { + val symbol = JvmSymbol( + key = "com/example/MyList", + sourceId = "foo.jar", + name = "com/example/MyList", + shortName = "MyList", + packageName = "com.example", + kind = JvmSymbolKind.TYPE_ALIAS, + language = JvmSourceLanguage.KOTLIN, + data = JvmTypeAliasInfo( + expandedTypeName = "java/util/ArrayList", + expandedTypeDisplayName = "ArrayList", + typeParameters = listOf("T"), + ), + ) + val data = roundtrip(symbol).data as JvmTypeAliasInfo + assertThat(data.expandedTypeName).isEqualTo("java/util/ArrayList") + assertThat(data.expandedTypeDisplayName).isEqualTo("ArrayList") + assertThat(data.typeParameters).containsExactly("T") + } + + @Test + fun `all JvmSymbolKind values roundtrip through serialization`() { + for (kind in JvmSymbolKind.entries) { + val data: JvmSymbolInfo = when { + kind == JvmSymbolKind.ENUM_ENTRY -> JvmEnumEntryInfo() + kind == JvmSymbolKind.TYPE_ALIAS -> JvmTypeAliasInfo() + kind == JvmSymbolKind.FIELD + || kind == JvmSymbolKind.PROPERTY + || kind == JvmSymbolKind.EXTENSION_PROPERTY -> JvmFieldInfo() + kind.isCallable -> JvmFunctionInfo() + else -> JvmClassInfo() + } + val symbol = JvmSymbol( + key = "k", sourceId = "s", name = "n", shortName = "n", + packageName = "p", kind = kind, + language = JvmSourceLanguage.KOTLIN, data = data, + ) + assertThat(roundtrip(symbol).kind).isEqualTo(kind) + } + } + + @Test + fun `all JvmVisibility values roundtrip`() { + for (vis in JvmVisibility.entries) { + val restored = roundtrip(classSymbol(visibility = vis)) + assertThat(restored.visibility).isEqualTo(vis) + } + } + + @Test + fun `all JvmSourceLanguage values roundtrip`() { + for (lang in JvmSourceLanguage.entries) { + val restored = roundtrip(classSymbol(language = lang)) + assertThat(restored.language).isEqualTo(lang) + } + } + + @Test + fun `isDeprecated flag roundtrips`() { + val s = classSymbol().copy(isDeprecated = true) + assertThat(roundtrip(s).isDeprecated).isTrue() + } + + @Test + fun `fieldValues extracts name shortName package kind language`() { + val symbol = classSymbol( + name = "com/example/Foo", + shortName = "Foo", + pkg = "com.example", + kind = JvmSymbolKind.CLASS, + language = JvmSourceLanguage.KOTLIN, + ) + val fields = JvmSymbolDescriptor.fieldValues(symbol) + assertThat(fields[JvmSymbolDescriptor.KEY_NAME]).isEqualTo("Foo") + assertThat(fields[JvmSymbolDescriptor.KEY_PACKAGE]).isEqualTo("com.example") + assertThat(fields[JvmSymbolDescriptor.KEY_KIND]).isEqualTo("CLASS") + assertThat(fields[JvmSymbolDescriptor.KEY_LANGUAGE]).isEqualTo("KOTLIN") + } + + @Test + fun `fieldValues containingClass is null for top-level class`() { + val fields = JvmSymbolDescriptor.fieldValues(classSymbol()) + assertThat(fields[JvmSymbolDescriptor.KEY_CONTAINING_CLASS]).isNull() + } + + @Test + fun `fieldValues containingClass is set for nested class`() { + val symbol = classSymbol(containingClass = "com/example/Outer") + val fields = JvmSymbolDescriptor.fieldValues(symbol) + assertThat(fields[JvmSymbolDescriptor.KEY_CONTAINING_CLASS]).isEqualTo("com/example/Outer") + } + + @Test + fun `fieldValues receiverType is null for regular function`() { + val symbol = JvmSymbol( + key = "f()", sourceId = "s", name = "f", shortName = "f", packageName = "p", + kind = JvmSymbolKind.FUNCTION, language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo(), + ) + assertThat(JvmSymbolDescriptor.fieldValues(symbol)[JvmSymbolDescriptor.KEY_RECEIVER_TYPE]) + .isNull() + } + + @Test + fun `fieldValues receiverType is set for extension function`() { + val symbol = JvmSymbol( + key = "f()", sourceId = "s", name = "f", shortName = "f", packageName = "p", + kind = JvmSymbolKind.EXTENSION_FUNCTION, language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo( + kotlin = KotlinFunctionInfo(receiverTypeName = "kotlin/collections/List"), + ), + ) + assertThat(JvmSymbolDescriptor.fieldValues(symbol)[JvmSymbolDescriptor.KEY_RECEIVER_TYPE]) + .isEqualTo("kotlin/collections/List") + } + + @Test + fun `descriptor name is jvm_symbols`() { + assertThat(JvmSymbolDescriptor.name).isEqualTo("jvm_symbols") + } + + @Test + fun `descriptor has 6 fields`() { + assertThat(JvmSymbolDescriptor.fields).hasSize(6) + } + + @Test + fun `name field is prefix-searchable`() { + val nameField = JvmSymbolDescriptor.fields.first { it.name == JvmSymbolDescriptor.KEY_NAME } + assertThat(nameField.prefixSearchable).isTrue() + } + + @Test + fun `other fields are not prefix-searchable`() { + val otherFields = JvmSymbolDescriptor.fields.filter { it.name != JvmSymbolDescriptor.KEY_NAME } + assertThat(otherFields.none { it.prefixSearchable }).isTrue() + } +} diff --git a/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndexTest.kt b/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndexTest.kt new file mode 100644 index 0000000000..c22b7da504 --- /dev/null +++ b/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndexTest.kt @@ -0,0 +1,315 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.appdevforall.codeonthego.indexing.InMemoryIndex +import org.appdevforall.codeonthego.indexing.util.BackgroundIndexer +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Unit tests for [JvmSymbolIndex] using an in-memory backing index. + * + * Each test activates the source before inserting so that [FilteredIndex] + * makes the entries visible. + */ +@RunWith(JUnit4::class) +class JvmSymbolIndexTest { + + private val defaultSource = "test.jar" + + private fun makeIndex(): JvmSymbolIndex { + val backing = InMemoryIndex(JvmSymbolDescriptor) + val indexer = BackgroundIndexer(backing) + return JvmSymbolIndex(backing, indexer) + } + + private fun classSymbol( + internalName: String, + pkg: String = internalName.substringBeforeLast('/').replace('/', '.'), + shortName: String = internalName.substringAfterLast('/'), + sourceId: String = defaultSource, + ) = JvmSymbol( + key = internalName, + sourceId = sourceId, + name = internalName, + shortName = shortName, + packageName = pkg, + kind = JvmSymbolKind.CLASS, + language = JvmSourceLanguage.KOTLIN, + data = JvmClassInfo(), + ) + + private fun funSymbol( + internalName: String, + shortName: String, + pkg: String, + containingClass: String = "", + receiverType: String = "", + sourceId: String = defaultSource, + params: List = emptyList(), + ): JvmSymbol { + val kind = if (receiverType.isNotEmpty()) JvmSymbolKind.EXTENSION_FUNCTION else JvmSymbolKind.FUNCTION + return JvmSymbol( + key = "$internalName(${params.joinToString(",") { it.typeName }})", + sourceId = sourceId, + name = internalName, + shortName = shortName, + packageName = pkg, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo( + containingClassName = containingClass, + parameters = params, + parameterCount = params.size, + kotlin = if (receiverType.isNotEmpty()) KotlinFunctionInfo(receiverTypeName = receiverType) else null, + ), + ) + } + + @Test + fun `findByPrefix returns symbols with matching name prefix`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insertAll( + sequenceOf( + classSymbol("com/example/ArrayList"), + classSymbol("com/example/ArrayDeque"), + classSymbol("com/example/String"), + ) + ) + + val results = index.findByPrefix("Array").map { it.shortName }.toList() + assertThat(results).containsExactly("ArrayList", "ArrayDeque") + } + + @Test + fun `findByPrefix is case-insensitive`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insert(classSymbol("com/example/ArrayList")) + + assertThat(index.findByPrefix("array").toList()).hasSize(1) + assertThat(index.findByPrefix("ARRAY").toList()).hasSize(1) + } + + @Test + fun `findByPrefix respects limit`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + (1..20).forEach { i -> index.insert(classSymbol("com/example/ArrayType$i")) } + + assertThat(index.findByPrefix("Array", limit = 5).toList()).hasSize(5) + } + + @Test + fun `findByPrefix returns empty when no source activated`() = runTest { + val index = makeIndex() + index.insert(classSymbol("com/example/ArrayList")) + // no activateSource call + + assertThat(index.findByPrefix("Array").toList()).isEmpty() + } + + @Test + fun `findByPrefix returns empty when prefix does not match`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insert(classSymbol("com/example/String")) + + assertThat(index.findByPrefix("Array").toList()).isEmpty() + } + + @Test + fun `findExtensionsFor returns extensions with given receiver type`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insertAll( + sequenceOf( + funSymbol("com/example/filter", "filter", "com.example", + receiverType = "kotlin/collections/List"), + funSymbol("com/example/map", "map", "com.example", + receiverType = "kotlin/collections/List"), + funSymbol("com/example/size", "size", "com.example", + receiverType = "kotlin/collections/Map"), + ) + ) + + val results = index.findExtensionsFor("kotlin/collections/List").map { it.shortName }.toList() + assertThat(results).containsExactly("filter", "map") + } + + @Test + fun `findExtensionsFor with namePrefix filters further`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insertAll( + sequenceOf( + funSymbol("com/example/filter", "filter", "com.example", + receiverType = "kotlin/collections/List"), + funSymbol("com/example/filterNot", "filterNot", "com.example", + receiverType = "kotlin/collections/List"), + funSymbol("com/example/map", "map", "com.example", + receiverType = "kotlin/collections/List"), + ) + ) + + val results = index.findExtensionsFor("kotlin/collections/List", namePrefix = "filter") + .map { it.shortName }.toList() + assertThat(results).containsExactly("filter", "filterNot") + } + + @Test + fun `findTopLevelCallablesInPackage returns only top-level callables`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + // Top-level fun + index.insert(funSymbol("com/example/topFun", "topFun", "com.example")) + // Member fun (has containing class → not top-level) + index.insert( + funSymbol( + "com/example/Foo.memberFun", "memberFun", "com.example", + containingClass = "com/example/Foo", + ) + ) + // Class (not callable) + index.insert(classSymbol("com/example/Foo")) + + val results = index.findTopLevelCallablesInPackage("com.example").map { it.shortName }.toList() + assertThat(results).containsExactly("topFun") + } + + @Test + fun `findTopLevelCallablesInPackage with namePrefix`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insert(funSymbol("com/example/filter", "filter", "com.example")) + index.insert(funSymbol("com/example/filterNot", "filterNot", "com.example")) + index.insert(funSymbol("com/example/map", "map", "com.example")) + + val results = index.findTopLevelCallablesInPackage("com.example", namePrefix = "filter") + .map { it.shortName }.toList() + assertThat(results).containsExactly("filter", "filterNot") + } + + @Test + fun `findClassifiersInPackage returns only classifiers`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insert(classSymbol("com/example/Foo")) + index.insert(classSymbol("com/example/Bar")) + index.insert(funSymbol("com/example/bazFun", "bazFun", "com.example")) + + val results = index.findClassifiersInPackage("com.example").map { it.shortName }.toList() + assertThat(results).containsExactly("Foo", "Bar") + } + + @Test + fun `findClassifiersInPackage with namePrefix`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insert(classSymbol("com/example/Array")) + index.insert(classSymbol("com/example/ArrayList")) + index.insert(classSymbol("com/example/String")) + + val results = index.findClassifiersInPackage("com.example", namePrefix = "Array") + .map { it.shortName }.toList() + assertThat(results).containsExactly("Array", "ArrayList") + } + + @Test + fun `findMembersOf returns symbols with given containing class`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insert(funSymbol("com/example/Foo.m1", "m1", "com.example", + containingClass = "com/example/Foo")) + index.insert(funSymbol("com/example/Foo.m2", "m2", "com.example", + containingClass = "com/example/Foo")) + index.insert(funSymbol("com/example/Bar.other", "other", "com.example", + containingClass = "com/example/Bar")) + + val results = index.findMembersOf("com/example/Foo").map { it.shortName }.toList() + assertThat(results).containsExactly("m1", "m2") + } + + @Test + fun `findMembersOf with namePrefix`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insert(funSymbol("com/example/Foo.get", "get", "com.example", + containingClass = "com/example/Foo")) + index.insert(funSymbol("com/example/Foo.getOrNull", "getOrNull", "com.example", + containingClass = "com/example/Foo")) + index.insert(funSymbol("com/example/Foo.set", "set", "com.example", + containingClass = "com/example/Foo")) + + val results = index.findMembersOf("com/example/Foo", namePrefix = "get") + .map { it.shortName }.toList() + assertThat(results).containsExactly("get", "getOrNull") + } + + @Test + fun `allPackages returns distinct packages from active sources`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + index.insert(classSymbol("com/example/Foo")) + index.insert(classSymbol("com/example/Bar")) + index.insert(classSymbol("org/other/Baz", pkg = "org.other")) + + val packages = index.allPackages().toSet() + assertThat(packages).containsAtLeast("com.example", "org.other") + } + + @Test + fun `findByKey retrieves symbol by exact key`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + val symbol = classSymbol("com/example/Foo") + index.insert(symbol) + + assertThat(index.findByKey("com/example/Foo")).isEqualTo(symbol) + } + + @Test + fun `findByKey returns null for unknown key`() = runTest { + val index = makeIndex() + index.activateSource(defaultSource) + assertThat(index.findByKey("missing")).isNull() + } + + @Test + fun `findByKey returns null when source not activated`() = runTest { + val index = makeIndex() + val symbol = classSymbol("com/example/Foo") + index.insert(symbol) + // no activateSource + assertThat(index.findByKey("com/example/Foo")).isNull() + } + + @Test + fun `symbols from inactive sources are not visible`() = runTest { + val index = makeIndex() + index.insert(classSymbol("com/a/A", sourceId = "a.jar")) + index.insert(classSymbol("com/b/B", sourceId = "b.jar")) + + index.activateSource("a.jar") + // b.jar not activated + + val results = index.findByPrefix("").toList() + assertThat(results.all { it.sourceId == "a.jar" }).isTrue() + } + + @Test + fun `activating multiple sources shows all their symbols`() = runTest { + val index = makeIndex() + index.insert(classSymbol("com/a/A", sourceId = "a.jar")) + index.insert(classSymbol("com/b/B", sourceId = "b.jar")) + + index.activateSource("a.jar") + index.activateSource("b.jar") + + val results = index.findByPrefix("").toList() + assertThat(results).hasSize(2) + } +} diff --git a/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolTest.kt b/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolTest.kt new file mode 100644 index 0000000000..6651cd2297 --- /dev/null +++ b/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolTest.kt @@ -0,0 +1,262 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class JvmSymbolTest { + + private fun classSymbol( + name: String = "com/example/Foo", + containingClass: String = "", + visibility: JvmVisibility = JvmVisibility.PUBLIC, + language: JvmSourceLanguage = JvmSourceLanguage.KOTLIN, + ) = JvmSymbol( + key = name, + sourceId = "test.jar", + name = name, + shortName = name.substringAfterLast('/').substringAfterLast('$'), + packageName = name.substringBeforeLast('/').replace('/', '.'), + kind = JvmSymbolKind.CLASS, + language = language, + visibility = visibility, + data = JvmClassInfo(containingClassName = containingClass), + ) + + private fun funSymbol( + name: String = "com/example/Foo.bar", + shortName: String = "bar", + kind: JvmSymbolKind = JvmSymbolKind.FUNCTION, + receiverType: String = "", + containingClass: String = "", + ) = JvmSymbol( + key = "$name()", + sourceId = "test.jar", + name = name, + shortName = shortName, + packageName = "com.example", + kind = kind, + language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo( + containingClassName = containingClass, + kotlin = if (receiverType.isNotEmpty()) KotlinFunctionInfo(receiverTypeName = receiverType) else null, + ), + ) + + @Test + fun `isTopLevel is true when containingClassName is empty`() { + assertThat(classSymbol(containingClass = "").isTopLevel).isTrue() + } + + @Test + fun `isTopLevel is false when containingClassName is non-empty`() { + assertThat(classSymbol(containingClass = "com/example/Outer").isTopLevel).isFalse() + } + + + @Test + fun `isExtension is true for EXTENSION_FUNCTION`() { + assertThat(funSymbol(kind = JvmSymbolKind.EXTENSION_FUNCTION).isExtension).isTrue() + } + + @Test + fun `isExtension is true for EXTENSION_PROPERTY`() { + val s = JvmSymbol( + key = "ext", sourceId = "s", name = "ext", shortName = "ext", + packageName = "p", kind = JvmSymbolKind.EXTENSION_PROPERTY, + language = JvmSourceLanguage.KOTLIN, + data = JvmFieldInfo(), + ) + assertThat(s.isExtension).isTrue() + } + + @Test + fun `isExtension is false for regular FUNCTION`() { + assertThat(funSymbol(kind = JvmSymbolKind.FUNCTION).isExtension).isFalse() + } + + @Test + fun `isExtension is false for CLASS`() { + assertThat(classSymbol().isExtension).isFalse() + } + + @Test + fun `fqName converts slashes to dots`() { + assertThat(classSymbol(name = "com/example/Foo").fqName).isEqualTo("com.example.Foo") + } + + @Test + fun `fqName converts dollars to dots for nested classes`() { + assertThat(classSymbol(name = "com/example/Outer\$Inner").fqName) + .isEqualTo("com.example.Outer.Inner") + } + + @Test + fun `containingClassFqName on JvmClassInfo converts correctly`() { + val info = JvmClassInfo(containingClassName = "com/example/Outer\$Inner") + assertThat(info.containingClassFqName).isEqualTo("com.example.Outer.Inner") + } + + @Test + fun `receiverTypeName extracted from EXTENSION_FUNCTION kotlin metadata`() { + val s = funSymbol(kind = JvmSymbolKind.EXTENSION_FUNCTION, receiverType = "kotlin/collections/List") + assertThat(s.receiverTypeName).isEqualTo("kotlin/collections/List") + } + + @Test + fun `receiverTypeName is null for non-extension function`() { + assertThat(funSymbol(kind = JvmSymbolKind.FUNCTION).receiverTypeName).isNull() + } + + @Test + fun `receiverTypeName is null when kotlin metadata receiver is empty`() { + val s = funSymbol(kind = JvmSymbolKind.EXTENSION_FUNCTION, receiverType = "") + assertThat(s.receiverTypeName).isNull() + } + + @Test + fun `receiverTypeName extracted from EXTENSION_PROPERTY field info`() { + val s = JvmSymbol( + key = "k", sourceId = "s", name = "n", shortName = "n", packageName = "p", + kind = JvmSymbolKind.EXTENSION_PROPERTY, + language = JvmSourceLanguage.KOTLIN, + data = JvmFieldInfo(kotlin = KotlinPropertyInfo(receiverTypeName = "kotlin/String")), + ) + assertThat(s.receiverTypeName).isEqualTo("kotlin/String") + } + + @Test + fun `returnTypeDisplay for function`() { + val s = JvmSymbol( + key = "f()", sourceId = "s", name = "f", shortName = "f", packageName = "p", + kind = JvmSymbolKind.FUNCTION, + language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo(returnTypeDisplayName = "String"), + ) + assertThat(s.returnTypeDisplay).isEqualTo("String") + } + + @Test + fun `returnTypeDisplay for field`() { + val s = JvmSymbol( + key = "f", sourceId = "s", name = "f", shortName = "f", packageName = "p", + kind = JvmSymbolKind.FIELD, + language = JvmSourceLanguage.JAVA, + data = JvmFieldInfo(typeDisplayName = "int"), + ) + assertThat(s.returnTypeDisplay).isEqualTo("int") + } + + @Test + fun `returnTypeDisplay is empty for class`() { + assertThat(classSymbol().returnTypeDisplay).isEmpty() + } + + @Test + fun `signatureDisplay for function`() { + val s = JvmSymbol( + key = "f()", sourceId = "s", name = "f", shortName = "f", packageName = "p", + kind = JvmSymbolKind.FUNCTION, + language = JvmSourceLanguage.KOTLIN, + data = JvmFunctionInfo(signatureDisplay = "fun f(x: Int): String"), + ) + assertThat(s.signatureDisplay).isEqualTo("fun f(x: Int): String") + } + + @Test + fun `signatureDisplay is empty for class`() { + assertThat(classSymbol().signatureDisplay).isEmpty() + } + + @Test + fun `isCallable is true for callable kinds`() { + assertThat(JvmSymbolKind.FUNCTION.isCallable).isTrue() + assertThat(JvmSymbolKind.EXTENSION_FUNCTION.isCallable).isTrue() + assertThat(JvmSymbolKind.CONSTRUCTOR.isCallable).isTrue() + assertThat(JvmSymbolKind.PROPERTY.isCallable).isTrue() + assertThat(JvmSymbolKind.EXTENSION_PROPERTY.isCallable).isTrue() + assertThat(JvmSymbolKind.FIELD.isCallable).isTrue() + } + + @Test + fun `isCallable is false for classifier kinds`() { + assertThat(JvmSymbolKind.CLASS.isCallable).isFalse() + assertThat(JvmSymbolKind.INTERFACE.isCallable).isFalse() + assertThat(JvmSymbolKind.ENUM.isCallable).isFalse() + assertThat(JvmSymbolKind.OBJECT.isCallable).isFalse() + assertThat(JvmSymbolKind.TYPE_ALIAS.isCallable).isFalse() + } + + @Test + fun `isClassifier is true for classifier kinds`() { + assertThat(JvmSymbolKind.CLASS.isClassifier).isTrue() + assertThat(JvmSymbolKind.INTERFACE.isClassifier).isTrue() + assertThat(JvmSymbolKind.ENUM.isClassifier).isTrue() + assertThat(JvmSymbolKind.DATA_CLASS.isClassifier).isTrue() + assertThat(JvmSymbolKind.VALUE_CLASS.isClassifier).isTrue() + assertThat(JvmSymbolKind.OBJECT.isClassifier).isTrue() + assertThat(JvmSymbolKind.COMPANION_OBJECT.isClassifier).isTrue() + assertThat(JvmSymbolKind.SEALED_CLASS.isClassifier).isTrue() + assertThat(JvmSymbolKind.SEALED_INTERFACE.isClassifier).isTrue() + assertThat(JvmSymbolKind.ANNOTATION_CLASS.isClassifier).isTrue() + assertThat(JvmSymbolKind.TYPE_ALIAS.isClassifier).isTrue() + } + + @Test + fun `isClassifier is false for callable kinds`() { + assertThat(JvmSymbolKind.FUNCTION.isClassifier).isFalse() + assertThat(JvmSymbolKind.PROPERTY.isClassifier).isFalse() + assertThat(JvmSymbolKind.FIELD.isClassifier).isFalse() + } + + @Test + fun `isAccessibleOutsideClass for PUBLIC, PROTECTED, INTERNAL`() { + assertThat(JvmVisibility.PUBLIC.isAccessibleOutsideClass).isTrue() + assertThat(JvmVisibility.PROTECTED.isAccessibleOutsideClass).isTrue() + assertThat(JvmVisibility.INTERNAL.isAccessibleOutsideClass).isTrue() + } + + @Test + fun `isAccessibleOutsideClass is false for PRIVATE and PACKAGE_PRIVATE`() { + assertThat(JvmVisibility.PRIVATE.isAccessibleOutsideClass).isFalse() + assertThat(JvmVisibility.PACKAGE_PRIVATE.isAccessibleOutsideClass).isFalse() + } + + @Test + fun `JvmFunctionInfo returnTypeFqName converts slashes and dollars`() { + val info = JvmFunctionInfo(returnTypeName = "com/example/Foo\$Bar") + assertThat(info.returnTypeFqName).isEqualTo("com.example.Foo.Bar") + } + + @Test + fun `JvmFieldInfo typeFqName converts slashes and dollars`() { + val info = JvmFieldInfo(typeName = "java/util/List") + assertThat(info.typeFqName).isEqualTo("java.util.List") + } + + @Test + fun `JvmParameterInfo typeFqName converts slashes and dollars`() { + val param = JvmParameterInfo(name = "p", typeName = "kotlin/String", typeDisplayName = "String") + assertThat(param.typeFqName).isEqualTo("kotlin.String") + } + + @Test + fun `JvmTypeAliasInfo expandedTypeFqName converts correctly`() { + val info = JvmTypeAliasInfo(expandedTypeName = "java/util/ArrayList") + assertThat(info.expandedTypeFqName).isEqualTo("java.util.ArrayList") + } + + @Test + fun `KotlinFunctionInfo receiverTypeFqName converts correctly`() { + val info = KotlinFunctionInfo(receiverTypeName = "kotlin/collections/List") + assertThat(info.receiverTypeFqName).isEqualTo("kotlin.collections.List") + } + + @Test + fun `KotlinPropertyInfo receiverTypeFqName converts correctly`() { + val info = KotlinPropertyInfo(receiverTypeName = "kotlin/String") + assertThat(info.receiverTypeFqName).isEqualTo("kotlin.String") + } +} diff --git a/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataTest.kt b/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataTest.kt new file mode 100644 index 0000000000..ae53cb72b3 --- /dev/null +++ b/lsp/jvm-symbol-index/src/test/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataTest.kt @@ -0,0 +1,175 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import java.time.Instant + +@RunWith(JUnit4::class) +class KtFileMetadataTest { + + @Test + fun `key equals filePath`() { + val meta = KtFileMetadata("/project/src/Main.kt", "com.example", Instant.now(), 1L) + assertThat(meta.key).isEqualTo("/project/src/Main.kt") + } + + @Test + fun `sourceId equals filePath`() { + val meta = KtFileMetadata("/project/src/Main.kt", "com.example", Instant.now(), 1L) + assertThat(meta.sourceId).isEqualTo("/project/src/Main.kt") + } + + @Test + fun `shouldBeSkipped is false when existing is null`() { + val meta = KtFileMetadata("/f.kt", "p", Instant.now(), 1L) + assertThat(KtFileMetadata.shouldBeSkipped(null, meta)).isFalse() + } + + @Test + fun `shouldBeSkipped is true when existing is same timestamp and stamp`() { + val ts = Instant.ofEpochMilli(500L) + val a = KtFileMetadata("/f.kt", "p", ts, 5L) + val b = KtFileMetadata("/f.kt", "p", ts, 5L) + assertThat(KtFileMetadata.shouldBeSkipped(a, b)).isTrue() + } + + @Test + fun `shouldBeSkipped is true when existing is newer by wall clock`() { + val older = KtFileMetadata("/f.kt", "p", Instant.ofEpochMilli(100L), 1L) + val newer = KtFileMetadata("/f.kt", "p", Instant.ofEpochMilli(200L), 2L) + // Existing is newer → skip + assertThat(KtFileMetadata.shouldBeSkipped(newer, older)).isTrue() + } + + @Test + fun `shouldBeSkipped is false when new file has newer wall clock`() { + val old = KtFileMetadata("/f.kt", "p", Instant.ofEpochMilli(100L), 1L) + val new = KtFileMetadata("/f.kt", "p", Instant.ofEpochMilli(200L), 2L) + assertThat(KtFileMetadata.shouldBeSkipped(old, new)).isFalse() + } + + @Test + fun `shouldBeSkipped is false when new has higher modificationStamp (same wall clock)`() { + val ts = Instant.ofEpochMilli(500L) + val existing = KtFileMetadata("/f.kt", "p", ts, 1L) + val new = KtFileMetadata("/f.kt", "p", ts, 2L) + // same wallclock but new has higher stamp + assertThat(KtFileMetadata.shouldBeSkipped(existing, new)).isFalse() + } + + @Test + fun `shouldBeSkipped is true when existing has higher modificationStamp (same wall clock)`() { + val ts = Instant.ofEpochMilli(500L) + val existing = KtFileMetadata("/f.kt", "p", ts, 3L) + val new = KtFileMetadata("/f.kt", "p", ts, 2L) + assertThat(KtFileMetadata.shouldBeSkipped(existing, new)).isTrue() + } + + @Test + fun `shouldBeSkipped handles zero modificationStamp symmetrically`() { + val ts = Instant.ofEpochMilli(100L) + val a = KtFileMetadata("/f.kt", "p", ts, 0L) + val b = KtFileMetadata("/f.kt", "p", ts, 0L) + // Both have stamp 0 — treated as "no stamp info available" + assertThat(KtFileMetadata.shouldBeSkipped(a, b)).isTrue() + } +} + +@RunWith(JUnit4::class) +class KtFileMetadataDescriptorTest { + + private fun roundtrip(meta: KtFileMetadata): KtFileMetadata = + KtFileMetadataDescriptor.deserialize(KtFileMetadataDescriptor.serialize(meta)) + + @Test + fun `serialize and deserialize preserves all fields`() { + val meta = KtFileMetadata( + filePath = "/project/src/Main.kt", + packageFqName = "com.example.main", + lastModified = Instant.ofEpochMilli(1_234_567_890L), + modificationStamp = 42L, + isIndexed = true, + symbolKeys = listOf("com.example.main.Foo", "com.example.main.Bar"), + ) + val restored = roundtrip(meta) + assertThat(restored.filePath).isEqualTo(meta.filePath) + assertThat(restored.packageFqName).isEqualTo(meta.packageFqName) + assertThat(restored.lastModified).isEqualTo(meta.lastModified) + assertThat(restored.modificationStamp).isEqualTo(meta.modificationStamp) + assertThat(restored.isIndexed).isTrue() + assertThat(restored.symbolKeys).containsExactlyElementsIn(meta.symbolKeys) + } + + @Test + fun `serialize with empty symbolKeys and isIndexed=false`() { + val meta = KtFileMetadata( + filePath = "/f.kt", + packageFqName = "", + lastModified = Instant.EPOCH, + modificationStamp = 0L, + isIndexed = false, + symbolKeys = emptyList(), + ) + val restored = roundtrip(meta) + assertThat(restored.symbolKeys).isEmpty() + assertThat(restored.isIndexed).isFalse() + assertThat(restored.packageFqName).isEmpty() + assertThat(restored.lastModified).isEqualTo(Instant.EPOCH) + } + + @Test + fun `serialize preserves large symbolKeys list`() { + val keys = (1..100).map { "com.example.Symbol$it" } + val meta = KtFileMetadata("/big.kt", "com.example", Instant.now(), 1L, + isIndexed = true, symbolKeys = keys) + assertThat(roundtrip(meta).symbolKeys).hasSize(100) + } + + @Test + fun `fieldValues returns package and isIndexed`() { + val meta = KtFileMetadata("/f.kt", "org.example", Instant.now(), 1L, isIndexed = true) + val fields = KtFileMetadataDescriptor.fieldValues(meta) + assertThat(fields[KtFileMetadataDescriptor.KEY_PACKAGE]).isEqualTo("org.example") + assertThat(fields[KtFileMetadataDescriptor.KEY_IS_INDEXED]).isEqualTo("true") + } + + @Test + fun `fieldValues isIndexed false`() { + val meta = KtFileMetadata("/f.kt", "p", Instant.now(), 1L, isIndexed = false) + assertThat(KtFileMetadataDescriptor.fieldValues(meta)[KtFileMetadataDescriptor.KEY_IS_INDEXED]) + .isEqualTo("false") + } + + @Test + fun `descriptor name is kt_file_metadata`() { + assertThat(KtFileMetadataDescriptor.name).isEqualTo("kt_file_metadata") + } + + @Test + fun `descriptor has exactly two fields`() { + assertThat(KtFileMetadataDescriptor.fields).hasSize(2) + assertThat(KtFileMetadataDescriptor.fields.map { it.name }) + .containsExactly( + KtFileMetadataDescriptor.KEY_PACKAGE, + KtFileMetadataDescriptor.KEY_IS_INDEXED, + ) + } + + @Test + fun `neither field is prefix-searchable`() { + assertThat(KtFileMetadataDescriptor.fields.none { it.prefixSearchable }).isTrue() + } + + @Test + fun `roundtrip with special characters in path`() { + val meta = KtFileMetadata( + filePath = "/Users/test user/my project/src/Main.kt", + packageFqName = "com.example", + lastModified = Instant.now(), + modificationStamp = 7L, + ) + assertThat(roundtrip(meta).filePath).isEqualTo(meta.filePath) + } +} diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index a4fcabb56b..be51051278 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -58,4 +58,5 @@ dependencies { compileOnly(projects.common) testImplementation(projects.testing.lsp) + testImplementation(libs.tests.kotlinx.coroutines) } diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywordsTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywordsTest.kt new file mode 100644 index 0000000000..a7a7e40911 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywordsTest.kt @@ -0,0 +1,175 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.kotlin.completion.DeclarationContext +import org.jetbrains.kotlin.lexer.KtTokens +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Unit tests for [ContextKeywords]. + * + * Verifies that [ContextKeywords.keywordsFor] returns the expected set of keyword + * tokens for each [DeclarationContext], and that the constant sets contain the + * expected members. + */ +@RunWith(JUnit4::class) +class ContextKeywordsTest { + + @Test + fun `STATEMENT_KEYWORDS contains core control-flow tokens`() { + assertThat(ContextKeywords.STATEMENT_KEYWORDS).containsAtLeast( + KtTokens.IF_KEYWORD, + KtTokens.ELSE_KEYWORD, + KtTokens.WHEN_KEYWORD, + KtTokens.WHILE_KEYWORD, + KtTokens.DO_KEYWORD, + KtTokens.FOR_KEYWORD, + KtTokens.TRY_KEYWORD, + KtTokens.RETURN_KEYWORD, + KtTokens.THROW_KEYWORD, + KtTokens.BREAK_KEYWORD, + ) + } + + @Test + fun `STATEMENT_KEYWORDS contains local declaration starters`() { + assertThat(ContextKeywords.STATEMENT_KEYWORDS).containsAtLeast( + KtTokens.VAL_KEYWORD, + KtTokens.VAR_KEYWORD, + KtTokens.FUN_KEYWORD, + ) + } + + @Test + fun `DECLARATION_KEYWORDS contains all top-level declaration starters`() { + assertThat(ContextKeywords.DECLARATION_KEYWORDS).containsAtLeast( + KtTokens.VAL_KEYWORD, + KtTokens.VAR_KEYWORD, + KtTokens.FUN_KEYWORD, + KtTokens.CLASS_KEYWORD, + KtTokens.INTERFACE_KEYWORD, + KtTokens.OBJECT_KEYWORD, + KtTokens.TYPE_ALIAS_KEYWORD, + ) + } + + @Test + fun `TOP_LEVEL_ONLY contains exactly PACKAGE and IMPORT`() { + assertThat(ContextKeywords.TOP_LEVEL_ONLY).containsExactly( + KtTokens.PACKAGE_KEYWORD, + KtTokens.IMPORT_KEYWORD, + ) + } + + @Test + fun `keywordsFor TOP_LEVEL includes TOP_LEVEL_ONLY tokens`() { + val keywords = ContextKeywords.keywordsFor(DeclarationContext.TOP_LEVEL) + assertThat(keywords).containsAtLeast(KtTokens.PACKAGE_KEYWORD, KtTokens.IMPORT_KEYWORD) + } + + @Test + fun `keywordsFor TOP_LEVEL includes all DECLARATION_KEYWORDS`() { + val keywords = ContextKeywords.keywordsFor(DeclarationContext.TOP_LEVEL) + assertThat(keywords).containsAtLeastElementsIn(ContextKeywords.DECLARATION_KEYWORDS) + } + + @Test + fun `keywordsFor SCRIPT_TOP_LEVEL equals keywordsFor TOP_LEVEL`() { + assertThat(ContextKeywords.keywordsFor(DeclarationContext.SCRIPT_TOP_LEVEL)) + .containsExactlyElementsIn(ContextKeywords.keywordsFor(DeclarationContext.TOP_LEVEL)) + } + + @Test + fun `keywordsFor FUNCTION_BODY equals STATEMENT_KEYWORDS`() { + assertThat(ContextKeywords.keywordsFor(DeclarationContext.FUNCTION_BODY)) + .containsExactlyElementsIn(ContextKeywords.STATEMENT_KEYWORDS) + } + + @Test + fun `keywordsFor FUNCTION_BODY does not include PACKAGE or IMPORT`() { + val keywords = ContextKeywords.keywordsFor(DeclarationContext.FUNCTION_BODY) + assertThat(keywords).doesNotContain(KtTokens.PACKAGE_KEYWORD) + assertThat(keywords).doesNotContain(KtTokens.IMPORT_KEYWORD) + } + + @Test + fun `keywordsFor FUNCTION_BODY does not include CLASS or INTERFACE`() { + val keywords = ContextKeywords.keywordsFor(DeclarationContext.FUNCTION_BODY) + // local class is included (rare but legal), interface is not + assertThat(keywords).doesNotContain(KtTokens.INTERFACE_KEYWORD) + } + + @Test + fun `keywordsFor CLASS_BODY includes INIT and CONSTRUCTOR`() { + val keywords = ContextKeywords.keywordsFor(DeclarationContext.CLASS_BODY) + assertThat(keywords).contains(KtTokens.INIT_KEYWORD) + assertThat(keywords).contains(KtTokens.CONSTRUCTOR_KEYWORD) + } + + @Test + fun `keywordsFor CLASS_BODY includes standard declaration starters`() { + val keywords = ContextKeywords.keywordsFor(DeclarationContext.CLASS_BODY) + assertThat(keywords).containsAtLeast( + KtTokens.VAL_KEYWORD, KtTokens.VAR_KEYWORD, KtTokens.FUN_KEYWORD, + ) + } + + @Test + fun `keywordsFor INTERFACE_BODY does not include CONSTRUCTOR`() { + assertThat(ContextKeywords.keywordsFor(DeclarationContext.INTERFACE_BODY)) + .doesNotContain(KtTokens.CONSTRUCTOR_KEYWORD) + } + + @Test + fun `keywordsFor INTERFACE_BODY does not include INIT`() { + assertThat(ContextKeywords.keywordsFor(DeclarationContext.INTERFACE_BODY)) + .doesNotContain(KtTokens.INIT_KEYWORD) + } + + @Test + fun `keywordsFor INTERFACE_BODY includes declaration starters for interface members`() { + val keywords = ContextKeywords.keywordsFor(DeclarationContext.INTERFACE_BODY) + assertThat(keywords).containsAtLeast( + KtTokens.VAL_KEYWORD, KtTokens.VAR_KEYWORD, KtTokens.FUN_KEYWORD, + ) + } + + @Test + fun `keywordsFor OBJECT_BODY does not include CONSTRUCTOR`() { + assertThat(ContextKeywords.keywordsFor(DeclarationContext.OBJECT_BODY)) + .doesNotContain(KtTokens.CONSTRUCTOR_KEYWORD) + } + + @Test + fun `keywordsFor ENUM_BODY does not include CONSTRUCTOR`() { + assertThat(ContextKeywords.keywordsFor(DeclarationContext.ENUM_BODY)) + .doesNotContain(KtTokens.CONSTRUCTOR_KEYWORD) + } + + @Test + fun `keywordsFor OBJECT_BODY and ENUM_BODY return same set`() { + assertThat(ContextKeywords.keywordsFor(DeclarationContext.OBJECT_BODY)) + .containsExactlyElementsIn(ContextKeywords.keywordsFor(DeclarationContext.ENUM_BODY)) + } + + @Test + fun `keywordsFor ANNOTATION_BODY returns only VAL`() { + assertThat(ContextKeywords.keywordsFor(DeclarationContext.ANNOTATION_BODY)) + .containsExactly(KtTokens.VAL_KEYWORD) + } + + @Test + fun `FUNCTION_BODY keywords are never in TOP_LEVEL_ONLY`() { + val functionKeywords = ContextKeywords.keywordsFor(DeclarationContext.FUNCTION_BODY) + assertThat(functionKeywords.none { it in ContextKeywords.TOP_LEVEL_ONLY }).isTrue() + } + + @Test + fun `all contexts return non-empty keyword sets`() { + for (ctx in DeclarationContext.entries) { + assertThat(ContextKeywords.keywordsFor(ctx)).isNotEmpty() + } + } +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityCheckerTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityCheckerTest.kt new file mode 100644 index 0000000000..c6fe6721a6 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityCheckerTest.kt @@ -0,0 +1,190 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.kotlin.compiler.services.ProjectStructureProvider +import io.mockk.mockk +import org.appdevforall.codeonthego.indexing.jvm.JvmClassInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmSourceLanguage +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolKind +import org.appdevforall.codeonthego.indexing.jvm.JvmVisibility +import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Unit tests for [SymbolVisibilityChecker.isDeclarationVisible]. + * + * The method is pure (depends only on [JvmSymbol.visibility], [JvmSymbol.packageName], + * and module identity comparisons), so it can be tested without a running K2 compiler. + */ +@RunWith(JUnit4::class) +class SymbolVisibilityCheckerTest { + + // Use relaxed mocks so that no stubs are required for unused methods + private val structureProvider = mockk(relaxed = true) + private val checker = SymbolVisibilityChecker(structureProvider) + + // Two distinct modules + private val moduleA: KaModule = mockk(relaxed = true) + private val moduleB: KaModule = mockk(relaxed = true) + + private fun symbol( + visibility: JvmVisibility, + pkg: String = "com.example", + sourceId: String = "jar-a", + ) = JvmSymbol( + key = "com/example/Foo", + sourceId = sourceId, + name = "com/example/Foo", + shortName = "Foo", + packageName = pkg, + kind = JvmSymbolKind.CLASS, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmClassInfo(), + ) + + @Test + fun `PUBLIC symbol is always visible regardless of module`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PUBLIC), + useSiteModule = moduleA, + declaringModule = moduleB, + useSitePackage = "other.pkg", + ) + ).isTrue() + } + + @Test + fun `PUBLIC symbol is visible even with null useSitePackage`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PUBLIC), + useSiteModule = moduleA, + declaringModule = moduleB, + useSitePackage = null, + ) + ).isTrue() + } + + @Test + fun `PRIVATE symbol is never visible`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PRIVATE), + useSiteModule = moduleA, + declaringModule = moduleB, + ) + ).isFalse() + } + + @Test + fun `PRIVATE symbol is not visible even from same module`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PRIVATE), + useSiteModule = moduleA, + declaringModule = moduleA, // same module + ) + ).isFalse() + } + + @Test + fun `INTERNAL symbol is visible from same module`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.INTERNAL), + useSiteModule = moduleA, + declaringModule = moduleA, + ) + ).isTrue() + } + + @Test + fun `INTERNAL symbol is not visible from different module`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.INTERNAL), + useSiteModule = moduleA, + declaringModule = moduleB, + ) + ).isFalse() + } + + @Test + fun `PACKAGE_PRIVATE symbol is visible within same package`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PACKAGE_PRIVATE, pkg = "com.example"), + useSiteModule = moduleA, + declaringModule = moduleB, + useSitePackage = "com.example", + ) + ).isTrue() + } + + @Test + fun `PACKAGE_PRIVATE symbol is not visible from different package`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PACKAGE_PRIVATE, pkg = "com.example"), + useSiteModule = moduleA, + declaringModule = moduleB, + useSitePackage = "org.other", + ) + ).isFalse() + } + + @Test + fun `PACKAGE_PRIVATE symbol is not visible when useSitePackage is null`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PACKAGE_PRIVATE, pkg = "com.example"), + useSiteModule = moduleA, + declaringModule = moduleB, + useSitePackage = null, + ) + ).isFalse() + } + + @Test + fun `PROTECTED symbol is visible within same package`() { + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PROTECTED, pkg = "com.example"), + useSiteModule = moduleA, + declaringModule = moduleB, + useSitePackage = "com.example", + ) + ).isTrue() + } + + @Test + fun `PROTECTED symbol is visible as assumed descendant across packages`() { + // isDescendant is hardcoded to true in the current implementation + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PROTECTED, pkg = "com.example"), + useSiteModule = moduleA, + declaringModule = moduleB, + useSitePackage = "different.pkg", + ) + ).isTrue() + } + + @Test + fun `PROTECTED symbol is visible with null useSitePackage due to descendant assumption`() { + // isSamePackage=false but isDescendant=true → visible + assertThat( + checker.isDeclarationVisible( + symbol(JvmVisibility.PROTECTED, pkg = "com.example"), + useSiteModule = moduleA, + declaringModule = moduleB, + useSitePackage = null, + ) + ).isTrue() + } +} diff --git a/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/models/ToolingApiTestLauncherParams.kt b/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/models/ToolingApiTestLauncherParams.kt index 82a1e261c1..03b1b56faf 100644 --- a/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/models/ToolingApiTestLauncherParams.kt +++ b/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/models/ToolingApiTestLauncherParams.kt @@ -18,6 +18,7 @@ package com.itsaky.androidide.testing.tooling.models import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher +import com.itsaky.androidide.tooling.api.messages.BuildId import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams import com.itsaky.androidide.utils.FileProvider import org.slf4j.Logger @@ -35,7 +36,8 @@ data class ToolingApiTestLauncherParams( val client: ToolingApiTestLauncher.MultiVersionTestClient = ToolingApiTestLauncher.MultiVersionTestClient(), val initParams: InitializeProjectParams = InitializeProjectParams( projectDir.pathString, - client.gradleDistParams + client.gradleDistParams, + buildId = BuildId.Unknown, ), val log: Logger = LoggerFactory.getLogger("BuildOutputLogger"), val sysProps: Map = emptyMap(), From ea6c1ca66edb6b0192582ed706602f75eeeaebfb Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 16 Apr 2026 19:48:02 +0530 Subject: [PATCH 50/58] feat: add snippets for Kotlin sources Signed-off-by: Akash Yadav --- .../itsaky/androidide/ui/CodeEditorView.kt | 2 + .../androidide/lsp/snippets/SnippetParser.kt | 2 +- .../snippet/JavaSnippetRepository.kt | 10 +- .../data/editor/kt/snippets.global.json | 36 ++++ .../assets/data/editor/kt/snippets.local.json | 156 ++++++++++++++++++ .../data/editor/kt/snippets.member.json | 62 +++++++ .../data/editor/kt/snippets.top-level.json | 72 ++++++++ .../lsp/kotlin/KotlinLanguageServer.kt | 3 + .../kotlin/completion/KotlinCompletions.kt | 69 +++++++- .../completion/KotlinSnippetRepository.kt | 13 ++ .../kotlin/completion/KotlinSnippetScope.kt | 26 +++ .../xml/providers/snippet/XmlSnippetScope.kt | 30 ++-- 12 files changed, 459 insertions(+), 22 deletions(-) create mode 100644 lsp/kotlin/src/main/assets/data/editor/kt/snippets.global.json create mode 100644 lsp/kotlin/src/main/assets/data/editor/kt/snippets.local.json create mode 100644 lsp/kotlin/src/main/assets/data/editor/kt/snippets.member.json create mode 100644 lsp/kotlin/src/main/assets/data/editor/kt/snippets.top-level.json create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt diff --git a/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt b/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt index fb661b77ac..dc8cbc7e59 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt @@ -46,6 +46,7 @@ import com.itsaky.androidide.lsp.IDELanguageClientImpl import com.itsaky.androidide.lsp.api.ILanguageServer import com.itsaky.androidide.lsp.api.ILanguageServerRegistry import com.itsaky.androidide.lsp.java.JavaLanguageServer +import com.itsaky.androidide.lsp.kotlin.KotlinLanguageServer import com.itsaky.androidide.lsp.xml.XMLLanguageServer import com.itsaky.androidide.models.Range import com.itsaky.androidide.preferences.internal.EditorPreferences @@ -545,6 +546,7 @@ class CodeEditorView( val serverID: String = when (file.extension) { "java" -> JavaLanguageServer.SERVER_ID + "kt", "kts" -> KotlinLanguageServer.SERVER_ID "xml" -> XMLLanguageServer.SERVER_ID else -> return null } diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt index 72ba79dbc3..83b75d195f 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt @@ -36,7 +36,7 @@ object SnippetParser { fun parse( lang: String, - scopes: Array, + scopes: Iterable, snippetFactory: (String, String, List) -> ISnippet = { prefix, desc, body -> DefaultSnippet(prefix, desc, body.toTypedArray()) }, diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt index 0562b72a84..da79badf12 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt @@ -27,10 +27,10 @@ import com.itsaky.androidide.lsp.snippets.SnippetParser */ object JavaSnippetRepository { - lateinit var snippets: Map> - private set + lateinit var snippets: Map> + private set - fun init() { - this.snippets = SnippetParser.parse("java", JavaSnippetScope.values()) - } + fun init() { + this.snippets = SnippetParser.parse("java", JavaSnippetScope.entries) + } } diff --git a/lsp/kotlin/src/main/assets/data/editor/kt/snippets.global.json b/lsp/kotlin/src/main/assets/data/editor/kt/snippets.global.json new file mode 100644 index 0000000000..087c37c124 --- /dev/null +++ b/lsp/kotlin/src/main/assets/data/editor/kt/snippets.global.json @@ -0,0 +1,36 @@ +{ + "lcomm": { + "desc": "Create a line comment", + "body": [ + "// $0" + ] + }, + "bcomm": { + "desc": "Create a block comment", + "body": [ + "/*", + " * $0", + " */" + ] + }, + "todo": { + "desc": "Create a TODO comment", + "body": [ + "// TODO: $0" + ] + }, + "fixme": { + "desc": "Create a FIXME comment", + "body": [ + "// FIXME: $0" + ] + }, + "kdoc": { + "desc": "Create a KDoc comment", + "body": [ + "/**", + " * $0", + " */" + ] + } +} diff --git a/lsp/kotlin/src/main/assets/data/editor/kt/snippets.local.json b/lsp/kotlin/src/main/assets/data/editor/kt/snippets.local.json new file mode 100644 index 0000000000..4714818d44 --- /dev/null +++ b/lsp/kotlin/src/main/assets/data/editor/kt/snippets.local.json @@ -0,0 +1,156 @@ +{ + "for": { + "desc": "For-in loop", + "body": [ + "for (${1:item} in ${2:collection}) {", + "\t$0", + "}" + ] + }, + "fori": { + "desc": "Indexed for loop", + "body": [ + "for (${1:i} in 0 until ${2:count}) {", + "\t$0", + "}" + ] + }, + "forr": { + "desc": "Reverse-indexed for loop", + "body": [ + "for (${1:i} in ${2:count} - 1 downTo 0) {", + "\t$0", + "}" + ] + }, + "while": { + "desc": "While loop", + "body": [ + "while (${1:condition}) {", + "\t$0", + "}" + ] + }, + "dowhile": { + "desc": "Do-while loop", + "body": [ + "do {", + "\t$0", + "} while (${1:condition})" + ] + }, + "if": { + "desc": "If statement", + "body": [ + "if (${1:condition}) {", + "\t$0", + "}" + ] + }, + "ifelse": { + "desc": "If-else statement", + "body": [ + "if (${1:condition}) {", + "\t$0", + "} else {", + "\t", + "}" + ] + }, + "ifnull": { + "desc": "If-null check", + "body": [ + "if (${1:obj} == null) {", + "\t$0", + "}" + ] + }, + "ifnotnull": { + "desc": "If-not-null check", + "body": [ + "if (${1:obj} != null) {", + "\t$0", + "}" + ] + }, + "when": { + "desc": "When expression", + "body": [ + "when (${1:expr}) {", + "\t$0", + "}" + ] + }, + "trycatch": { + "desc": "Try-catch statement", + "body": [ + "try {", + "\t$0", + "} catch (${1:e}: ${2:Exception}) {", + "\t", + "}" + ] + }, + "tryfinally": { + "desc": "Try-finally statement", + "body": [ + "try {", + "\t$0", + "} finally {", + "\t", + "}" + ] + }, + "let": { + "desc": "Safe-call let scope function", + "body": [ + "${1:obj}?.let { ${2:it} ->", + "\t$0", + "}" + ] + }, + "apply": { + "desc": "Apply scope function", + "body": [ + "${1:obj}.apply {", + "\t$0", + "}" + ] + }, + "also": { + "desc": "Also scope function", + "body": [ + "${1:obj}.also { ${2:it} ->", + "\t$0", + "}" + ] + }, + "run": { + "desc": "Run scope function", + "body": [ + "${1:obj}.run {", + "\t$0", + "}" + ] + }, + "with": { + "desc": "With scope function", + "body": [ + "with(${1:obj}) {", + "\t$0", + "}" + ] + }, + "require": { + "desc": "Require precondition check", + "body": [ + "require(${1:condition}) { \"${2:message}\" }" + ] + }, + "check": { + "desc": "Check state assertion", + "body": [ + "check(${1:condition}) { \"${2:message}\" }" + ] + } +} diff --git a/lsp/kotlin/src/main/assets/data/editor/kt/snippets.member.json b/lsp/kotlin/src/main/assets/data/editor/kt/snippets.member.json new file mode 100644 index 0000000000..ff07d33b7b --- /dev/null +++ b/lsp/kotlin/src/main/assets/data/editor/kt/snippets.member.json @@ -0,0 +1,62 @@ +{ + "fun": { + "desc": "Create a member function", + "body": [ + "fun ${1:name}($2)${3:: Unit} {", + "\t$0", + "}" + ] + }, + "pfun": { + "desc": "Create a private function", + "body": [ + "private fun ${1:name}($2)${3:: Unit} {", + "\t$0", + "}" + ] + }, + "ofun": { + "desc": "Create an override function", + "body": [ + "override fun ${1:name}($2)${3:: Unit} {", + "\t$0", + "}" + ] + }, + "val": { + "desc": "Create a val property", + "body": [ + "val ${1:name}: ${2:Type} = ${3:value}" + ] + }, + "var": { + "desc": "Create a var property", + "body": [ + "var ${1:name}: ${2:Type} = ${3:value}" + ] + }, + "lazy": { + "desc": "Lazy-initialized property", + "body": [ + "val ${1:name} by lazy {", + "\t$0", + "}" + ] + }, + "comp": { + "desc": "Create a companion object", + "body": [ + "companion object {", + "\t$0", + "}" + ] + }, + "init": { + "desc": "Create an initializer block", + "body": [ + "init {", + "\t$0", + "}" + ] + } +} diff --git a/lsp/kotlin/src/main/assets/data/editor/kt/snippets.top-level.json b/lsp/kotlin/src/main/assets/data/editor/kt/snippets.top-level.json new file mode 100644 index 0000000000..b6fd6b9969 --- /dev/null +++ b/lsp/kotlin/src/main/assets/data/editor/kt/snippets.top-level.json @@ -0,0 +1,72 @@ +{ + "class": { + "desc": "Create a new class", + "body": [ + "class ${1:$TM_FILENAME_BASE} {", + "\t$0", + "}" + ] + }, + "dclass": { + "desc": "Create a new data class", + "body": [ + "data class ${1:$TM_FILENAME_BASE}(", + "\t${2:val prop: Type}", + ")" + ] + }, + "sclass": { + "desc": "Create a new sealed class", + "body": [ + "sealed class ${1:$TM_FILENAME_BASE} {", + "\t$0", + "}" + ] + }, + "interface": { + "desc": "Create a new interface", + "body": [ + "interface ${1:$TM_FILENAME_BASE} {", + "\t$0", + "}" + ] + }, + "sinterface": { + "desc": "Create a new sealed interface", + "body": [ + "sealed interface ${1:$TM_FILENAME_BASE} {", + "\t$0", + "}" + ] + }, + "enum": { + "desc": "Create a new enum class", + "body": [ + "enum class ${1:$TM_FILENAME_BASE} {", + "\t$0", + "}" + ] + }, + "object": { + "desc": "Create a new object declaration", + "body": [ + "object ${1:$TM_FILENAME_BASE} {", + "\t$0", + "}" + ] + }, + "const": { + "desc": "Create a compile-time constant", + "body": [ + "const val ${1:NAME} = ${2:value}" + ] + }, + "fun": { + "desc": "Create a top-level function", + "body": [ + "fun ${1:name}($2)${3:: Unit} {", + "\t$0", + "}" + ] + } +} 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 46d1062f19..517ac4b557 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 @@ -30,6 +30,7 @@ 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.KotlinSnippetRepository import com.itsaky.androidide.lsp.kotlin.completion.complete import com.itsaky.androidide.lsp.kotlin.diagnostic.collectDiagnosticsFor import com.itsaky.androidide.lsp.models.CompletionParams @@ -106,6 +107,8 @@ class KotlinLanguageServer : ILanguageServer { if (!EventBus.getDefault().isRegistered(this)) { EventBus.getDefault().register(this) } + + KotlinSnippetRepository.init() } override fun shutdown() { 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 be58063f7b..dd92d771b3 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 @@ -15,9 +15,9 @@ import com.itsaky.androidide.lsp.models.CompletionItemKind import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.InsertTextFormat +import com.itsaky.androidide.preferences.utils.indentationString import com.itsaky.androidide.projects.FileManager import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.runBlocking import org.appdevforall.codeonthego.indexing.jvm.JvmClassInfo import org.appdevforall.codeonthego.indexing.jvm.JvmFunctionInfo import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol @@ -55,9 +55,14 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtClassBody import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression +import org.jetbrains.kotlin.psi.KtWhenExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.types.Variance @@ -143,6 +148,7 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi } collectKeywordCompletions(to = items) + collectSnippetCompletions(to = items) CompletionResult(items) } } @@ -420,6 +426,67 @@ private fun KaSession.collectKeywordCompletions( } } +context(ctx: AnalysisContext) +private fun KaSession.collectSnippetCompletions(to: MutableList) { + val snippets = buildList { + // add global snippets, if any + KotlinSnippetRepository.snippets[KotlinSnippetScope.GLOBAL]?.also { addAll(it) } + + val snippetScope = when (ctx.declarationKind) { + DeclarationKind.CLASS , + DeclarationKind.INTERFACE , + DeclarationKind.OBJECT , + DeclarationKind.ENUM_CLASS , + DeclarationKind.ANNOTATION_CLASS -> KotlinSnippetScope.MEMBER + + DeclarationKind.CONSTRUCTOR, + DeclarationKind.FUN -> KotlinSnippetScope.LOCAL + + DeclarationKind.UNKNOWN -> KotlinSnippetScope.TOP_LEVEL.takeIf { ctx.declarationContext == DeclarationContext.TOP_LEVEL } + + DeclarationKind.PROPERTY_VAL -> null + DeclarationKind.PROPERTY_VAR -> null + DeclarationKind.TYPEALIAS -> null + } + + logger.info("Adding completions for snippet scope: {} (context: {}, kind: {})", snippetScope, ctx.declarationContext, ctx.declarationKind) + KotlinSnippetRepository.snippets[snippetScope]?.also { addAll(it) } + } + + val indent = computeIndentLevelAt(ctx.ktElement) + for (snippet in snippets) { + to += ktCompletionItem(snippet.prefix, CompletionItemKind.SNIPPET).apply { + detail = snippet.description + ideSortText = "00000${snippet.prefix}" + snippetDescription = describeSnippet(ctx.partial) + + val indentation = indentationString(indent) + insertTextFormat = InsertTextFormat.SNIPPET + insertText = snippet.body.joinToString(separator = System.lineSeparator()) { + it.replace("\t", indentation) + .replace("\n", "\n${indentation}") + } + } + } +} + +private fun computeIndentLevelAt(ktElement: KtElement): Int { + var indentLevel = 0 + var current = ktElement.parent + + while (current != null) { + if (current is KtBlockExpression || + current is KtClassBody || + current is KtWhenExpression || + current is KtFunction) { + indentLevel++ + } + current = current.parent + } + + return indentLevel +} + context(ctx: AnalysisContext) @JvmName("callablesToCompletionItems") private fun KaSession.toCompletionItems( diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt new file mode 100644 index 0000000000..e62a82e941 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt @@ -0,0 +1,13 @@ +package com.itsaky.androidide.lsp.kotlin.completion + +import com.itsaky.androidide.lsp.snippets.ISnippet +import com.itsaky.androidide.lsp.snippets.SnippetParser + +object KotlinSnippetRepository { + lateinit var snippets: Map> + private set + + fun init() { + snippets = SnippetParser.parse("kt", KotlinSnippetScope.entries) + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt new file mode 100644 index 0000000000..a3d5a9d47b --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt @@ -0,0 +1,26 @@ +package com.itsaky.androidide.lsp.kotlin.completion + +import com.itsaky.androidide.lsp.snippets.ISnippetScope + +/** + * Snippet scopes for Kotlin source files. + * + * @author Akash Yadav + */ +enum class KotlinSnippetScope(override val filename: String) : ISnippetScope { + + /** + * Snippets that can be used at the top level. This includes snippets such as class, interface, + * enum templates. + */ + TOP_LEVEL("top-level"), + + /** Snippets that can be used at the member level i.e. inside a class tree. */ + MEMBER("member"), + + /** Snippets that can be used at a local level. E.g. inside a method or constructor. */ + LOCAL("local"), + + /** Snippets that can be used anywhere in the code, irrespective of the current scope. */ + GLOBAL("global"), +} \ No newline at end of file diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/snippet/XmlSnippetScope.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/snippet/XmlSnippetScope.kt index af9d262b06..0928747fb9 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/snippet/XmlSnippetScope.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/snippet/XmlSnippetScope.kt @@ -19,30 +19,30 @@ package com.itsaky.androidide.lsp.xml.providers.snippet import com.itsaky.androidide.lsp.snippets.ISnippetScope -val XML_SNIPPET_SCOPES : Array = - arrayOf( - DefaultXmlSnippetScope(XmlResourceType.LAYOUT, XmlScope.TAG), - DefaultXmlSnippetScope(XmlResourceType.LAYOUT, XmlScope.INSIDE), - DefaultXmlSnippetScope(XmlResourceType.MANIFEST, XmlScope.INSIDE) - ) +val XML_SNIPPET_SCOPES = + listOf( + DefaultXmlSnippetScope(XmlResourceType.LAYOUT, XmlScope.TAG), + DefaultXmlSnippetScope(XmlResourceType.LAYOUT, XmlScope.INSIDE), + DefaultXmlSnippetScope(XmlResourceType.MANIFEST, XmlScope.INSIDE) + ) abstract class IXmlSnippetScope : ISnippetScope { - abstract val type: XmlResourceType - abstract val scope: XmlScope + abstract val type: XmlResourceType + abstract val scope: XmlScope - override val filename: String - get() = "${type.name.lowercase()}-${scope.name.lowercase()}" + override val filename: String + get() = "${type.name.lowercase()}-${scope.name.lowercase()}" } class DefaultXmlSnippetScope(override val type: XmlResourceType, override val scope: XmlScope) : - IXmlSnippetScope() + IXmlSnippetScope() enum class XmlResourceType { - LAYOUT, - MANIFEST + LAYOUT, + MANIFEST } enum class XmlScope { - TAG, - INSIDE + TAG, + INSIDE } From f29e83ade60017c85d25f0627a6f61a0245a1e08 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 16 Apr 2026 19:51:41 +0530 Subject: [PATCH 51/58] feat: make comment/uncomment line actions generic to LSP implementations Signed-off-by: Akash Yadav --- lsp/api/build.gradle.kts | 1 + .../lsp/actions/CommentLineAction.kt | 85 +++++++++++++++++ .../lsp/actions/UncommentLineAction.kt | 91 +++++++++++++++++++ .../lsp/java/actions/JavaCodeActionsMenu.kt | 54 +++++------ .../lsp/java/actions/common/CommentAction.kt | 50 ---------- .../java/actions/common/UncommentAction.kt | 54 ----------- 6 files changed, 204 insertions(+), 131 deletions(-) create mode 100644 lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt create mode 100644 lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt delete mode 100644 lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/CommentAction.kt delete mode 100644 lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/UncommentAction.kt diff --git a/lsp/api/build.gradle.kts b/lsp/api/build.gradle.kts index 2aab922a93..e728203e1b 100644 --- a/lsp/api/build.gradle.kts +++ b/lsp/api/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { api(projects.lookup) api(projects.lsp.models) api(projects.preferences) + api(projects.idetooltips) compileOnly(projects.actions) compileOnly(projects.common) diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt new file mode 100644 index 0000000000..21a4837d09 --- /dev/null +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt @@ -0,0 +1,85 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.androidide.lsp.actions + +import android.content.Context +import android.graphics.drawable.Drawable +import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.actions.ActionItem +import com.itsaky.androidide.actions.EditorActionItem +import com.itsaky.androidide.actions.hasRequiredData +import com.itsaky.androidide.actions.markInvisible +import com.itsaky.androidide.actions.requireContext +import com.itsaky.androidide.actions.requireEditor +import com.itsaky.androidide.actions.requireFile +import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.resources.R +import io.github.rosemoe.sora.text.batchEdit +import java.io.File + +/** @author Akash Yadav */ +class CommentLineAction( + private val targetFileExtensions: List, + private val lineCommentToken: String, +) : EditorActionItem { + + constructor(extension: String, lineCommentToken: String) : + this(listOf(extension), lineCommentToken) + override val id: String = "ide.editor.lsp.java.commentLine" + override var label: String = "" + + override var visible = true + override var enabled = true + override var icon: Drawable? = null + override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS + override var requiresUIThread: Boolean = true + override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_COMMENT + + override fun prepare(data: ActionData) { + super.prepare(data) + + if (!data.hasRequiredData(Context::class.java, File::class.java)) { + markInvisible() + return + } + + val context = data.requireContext() + label = context.getString(R.string.action_comment_line) + + val file = data.requireFile() + if (file.extension !in targetFileExtensions) { + markInvisible() + return + } + } + + override suspend fun execAction(data: ActionData): Boolean { + val editor = data.requireEditor() + val text = editor.text + val cursor = editor.cursor + + text.batchEdit { + for (line in cursor.leftLine..cursor.rightLine) { + text.insert(line, 0, lineCommentToken) + } + } + + return true + } + + override fun dismissOnAction() = true +} diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt new file mode 100644 index 0000000000..f9c83e5e67 --- /dev/null +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt @@ -0,0 +1,91 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.androidide.lsp.actions + +import android.content.Context +import android.graphics.drawable.Drawable +import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.actions.ActionItem +import com.itsaky.androidide.actions.EditorActionItem +import com.itsaky.androidide.actions.hasRequiredData +import com.itsaky.androidide.actions.markInvisible +import com.itsaky.androidide.actions.requireContext +import com.itsaky.androidide.actions.requireEditor +import com.itsaky.androidide.actions.requireFile +import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.resources.R +import io.github.rosemoe.sora.text.batchEdit +import java.io.File + +/** @author Akash Yadav */ +class UncommentLineAction( + private val targetFileExtensions: List, + private val lineCommentToken: String, +) : EditorActionItem { + + constructor(extension: String, lineCommentToken: String) : + this(listOf(extension), lineCommentToken) + + override val id: String = "ide.editor.lsp.java.uncommentLine" + override var label: String = "" + + override var visible = true + override var enabled = true + override var icon: Drawable? = null + override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS + override var requiresUIThread: Boolean = true + override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_UNCOMMENT + + + override fun prepare(data: ActionData) { + super.prepare(data) + + if (!data.hasRequiredData(Context::class.java, File::class.java)) { + markInvisible() + return + } + + val context = data.requireContext() + label = context.getString(R.string.action_comment_line) + + val file = data.requireFile() + if (file.extension !in targetFileExtensions) { + markInvisible() + return + } + } + + override suspend fun execAction(data: ActionData): Boolean { + val editor = data.requireEditor() + val text = editor.text + val cursor = editor.cursor + + text.batchEdit { + for (line in cursor.leftLine..cursor.rightLine) { + val l = text.getLineString(line) + if (l.trim().startsWith(lineCommentToken)) { + val i = l.indexOf(lineCommentToken) + text.delete(line, i, line, i + 2) + } + } + } + + return true + } + + override fun dismissOnAction() = true +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt index 8d106784c7..ab870a6a3e 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt @@ -19,12 +19,12 @@ package com.itsaky.androidide.lsp.java.actions import com.itsaky.androidide.actions.ActionItem import com.itsaky.androidide.lsp.actions.IActionsMenuProvider -import com.itsaky.androidide.lsp.java.actions.common.CommentAction +import com.itsaky.androidide.lsp.actions.CommentLineAction import com.itsaky.androidide.lsp.java.actions.common.FindReferencesAction import com.itsaky.androidide.lsp.java.actions.common.GoToDefinitionAction import com.itsaky.androidide.lsp.java.actions.common.OrganizeImportsAction import com.itsaky.androidide.lsp.java.actions.common.RemoveUnusedImportsAction -import com.itsaky.androidide.lsp.java.actions.common.UncommentAction +import com.itsaky.androidide.lsp.actions.UncommentLineAction import com.itsaky.androidide.lsp.java.actions.diagnostics.AddImportAction import com.itsaky.androidide.lsp.java.actions.diagnostics.AddThrowsAction import com.itsaky.androidide.lsp.java.actions.diagnostics.AutoFixImportsAction @@ -48,29 +48,29 @@ import com.itsaky.androidide.lsp.java.actions.generators.OverrideSuperclassMetho */ object JavaCodeActionsMenu : IActionsMenuProvider { - override val actions: List = - listOf( - CommentAction(), - UncommentAction(), - GoToDefinitionAction(), - FindReferencesAction(), - AddImportAction(), - AutoFixImportsAction(), - ImplementAbstractMethodsAction(), - VariableToStatementAction(), - FieldToBlockAction(), - RemoveClassAction(), - RemoveMethodAction(), - RemoveUnusedThrowsAction(), - CreateMissingMethodAction(), - SuppressUncheckedWarningAction(), - AddThrowsAction(), - GenerateSettersAndGettersAction(), - OverrideSuperclassMethodsAction(), - GenerateMissingConstructorAction(), - GenerateConstructorAction(), - GenerateToStringMethodAction(), - RemoveUnusedImportsAction(), - OrganizeImportsAction() - ) + override val actions: List = + listOf( + CommentLineAction("java", "//"), + UncommentLineAction("java", "//"), + GoToDefinitionAction(), + FindReferencesAction(), + AddImportAction(), + AutoFixImportsAction(), + ImplementAbstractMethodsAction(), + VariableToStatementAction(), + FieldToBlockAction(), + RemoveClassAction(), + RemoveMethodAction(), + RemoveUnusedThrowsAction(), + CreateMissingMethodAction(), + SuppressUncheckedWarningAction(), + AddThrowsAction(), + GenerateSettersAndGettersAction(), + OverrideSuperclassMethodsAction(), + GenerateMissingConstructorAction(), + GenerateConstructorAction(), + GenerateToStringMethodAction(), + RemoveUnusedImportsAction(), + OrganizeImportsAction() + ) } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/CommentAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/CommentAction.kt deleted file mode 100644 index d15f9e2bac..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/CommentAction.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ -package com.itsaky.androidide.lsp.java.actions.common - -import com.itsaky.androidide.actions.ActionData -import com.itsaky.androidide.actions.requireEditor -import com.itsaky.androidide.idetooltips.TooltipTag -import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction -import com.itsaky.androidide.resources.R - -/** @author Akash Yadav */ -class CommentAction : BaseJavaCodeAction() { - override val id: String = "ide.editor.lsp.java.commentLine" - override var label: String = "" - - override val titleTextRes: Int = R.string.action_comment_line - - override var requiresUIThread: Boolean = true - override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_COMMENT - - override suspend fun execAction(data: ActionData): Boolean { - val editor = data.requireEditor() - val text = editor.text - val cursor = editor.cursor - - text.beginBatchEdit() - for (line in cursor.leftLine..cursor.rightLine) { - text.insert(line, 0, "//") - } - text.endBatchEdit() - - return true - } - - override fun dismissOnAction() = true -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/UncommentAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/UncommentAction.kt deleted file mode 100644 index 9c9978fe7f..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/UncommentAction.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ -package com.itsaky.androidide.lsp.java.actions.common - -import com.itsaky.androidide.actions.ActionData -import com.itsaky.androidide.actions.requireEditor -import com.itsaky.androidide.idetooltips.TooltipTag -import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction -import com.itsaky.androidide.resources.R - -/** @author Akash Yadav */ -class UncommentAction : BaseJavaCodeAction() { - override val id: String = "ide.editor.lsp.java.uncommentLine" - override var label: String = "" - - override val titleTextRes: Int = R.string.action_uncomment_line - - override var requiresUIThread: Boolean = true - override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_UNCOMMENT - - override suspend fun execAction(data: ActionData): Boolean { - val editor = data.requireEditor() - val text = editor.text - val cursor = editor.cursor - - text.beginBatchEdit() - for (line in cursor.leftLine..cursor.rightLine) { - val l = text.getLineString(line) - if (l.trim().startsWith("//")) { - val i = l.indexOf("//") - text.delete(line, i, line, i + 2) - } - } - text.endBatchEdit() - - return true - } - - override fun dismissOnAction() = true -} From 9e1883426f0a4dee76a008204190bf69cb62200e Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 17 Apr 2026 00:26:36 +0530 Subject: [PATCH 52/58] fix: register comment/uncomment line actions in Kotlin LSP Signed-off-by: Akash Yadav --- lsp/kotlin/build.gradle.kts | 1 + .../lsp/kotlin/KotlinCodeActionsMenu.kt | 18 ++++++++++++++++++ .../lsp/kotlin/KotlinLanguageServer.kt | 3 +++ 3 files changed, 22 insertions(+) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index be51051278..2fb68ceecd 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -40,6 +40,7 @@ kapt { dependencies { kapt(projects.annotationProcessors) + implementation(projects.actions) implementation(projects.lsp.api) implementation(projects.lsp.jvmSymbolIndex) implementation(projects.lsp.models) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt new file mode 100644 index 0000000000..1faf18e181 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt @@ -0,0 +1,18 @@ +package com.itsaky.androidide.lsp.kotlin + +import com.itsaky.androidide.actions.ActionItem +import com.itsaky.androidide.lsp.actions.CommentLineAction +import com.itsaky.androidide.lsp.actions.IActionsMenuProvider +import com.itsaky.androidide.lsp.actions.UncommentLineAction + +object KotlinCodeActionsMenu : IActionsMenuProvider { + + private val KT_EXTS = listOf("kt", "kts") + private const val KT_LINE_COMMENT_TOKEN = "//" + + override val actions: List = + listOf( + CommentLineAction(KT_EXTS, KT_LINE_COMMENT_TOKEN), + UncommentLineAction(KT_EXTS, KT_LINE_COMMENT_TOKEN) + ) +} \ No newline at end of file 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 517ac4b557..1cd9653187 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 @@ -43,6 +43,7 @@ import com.itsaky.androidide.lsp.models.ReferenceParams import com.itsaky.androidide.lsp.models.ReferenceResult import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams +import com.itsaky.androidide.lsp.util.LSPEditorActions import com.itsaky.androidide.models.Range import com.itsaky.androidide.projects.FileManager import com.itsaky.androidide.projects.ProjectManagerImpl @@ -129,6 +130,8 @@ class KotlinLanguageServer : ILanguageServer { override fun setupWithProject(workspace: Workspace) { logger.info("setupWithProject called, initialized={}", initialized) + LSPEditorActions.ensureActionsMenuRegistered(KotlinCodeActionsMenu) + val context = BaseApplication.baseInstance val indexingServiceManager = ProjectManagerImpl.getInstance() .indexingServiceManager From c569cb68ae0dc3ed3a5e4420d0f46bba5b80b5fa Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 17 Apr 2026 00:58:20 +0530 Subject: [PATCH 53/58] fix: comment/uncomment actions are overridden by language servers Signed-off-by: Akash Yadav --- .../itsaky/androidide/lsp/actions/CommentLineAction.kt | 7 ++++--- .../itsaky/androidide/lsp/actions/UncommentLineAction.kt | 7 ++++--- .../androidide/lsp/java/actions/JavaCodeActionsMenu.kt | 8 ++++++-- .../itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt | 5 +++-- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt index 21a4837d09..d95d804fd3 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt @@ -33,13 +33,14 @@ import java.io.File /** @author Akash Yadav */ class CommentLineAction( + lang: String, private val targetFileExtensions: List, private val lineCommentToken: String, ) : EditorActionItem { - constructor(extension: String, lineCommentToken: String) : - this(listOf(extension), lineCommentToken) - override val id: String = "ide.editor.lsp.java.commentLine" + constructor(lang: String, extension: String, lineCommentToken: String) : + this(lang, listOf(extension), lineCommentToken) + override val id: String = "ide.editor.lsp.$lang.commentLine" override var label: String = "" override var visible = true diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt index f9c83e5e67..78d632d557 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt @@ -33,14 +33,15 @@ import java.io.File /** @author Akash Yadav */ class UncommentLineAction( + lang: String, private val targetFileExtensions: List, private val lineCommentToken: String, ) : EditorActionItem { - constructor(extension: String, lineCommentToken: String) : - this(listOf(extension), lineCommentToken) + constructor(lang: String, extension: String, lineCommentToken: String) : + this(lang, listOf(extension), lineCommentToken) - override val id: String = "ide.editor.lsp.java.uncommentLine" + override val id: String = "ide.editor.lsp.$lang.uncommentLine" override var label: String = "" override var visible = true diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt index ab870a6a3e..1f1b3ecdfc 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt @@ -48,10 +48,14 @@ import com.itsaky.androidide.lsp.java.actions.generators.OverrideSuperclassMetho */ object JavaCodeActionsMenu : IActionsMenuProvider { + private const val LANG = "java" + private const val EXT = "java" + private const val LINE_COMMENT_TOKEN = "//" + override val actions: List = listOf( - CommentLineAction("java", "//"), - UncommentLineAction("java", "//"), + CommentLineAction(LANG, EXT, LINE_COMMENT_TOKEN), + UncommentLineAction(LANG, EXT, LINE_COMMENT_TOKEN), GoToDefinitionAction(), FindReferencesAction(), AddImportAction(), diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt index 1faf18e181..4e451059a8 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt @@ -7,12 +7,13 @@ import com.itsaky.androidide.lsp.actions.UncommentLineAction object KotlinCodeActionsMenu : IActionsMenuProvider { + private const val KT_LANG = "kt" private val KT_EXTS = listOf("kt", "kts") private const val KT_LINE_COMMENT_TOKEN = "//" override val actions: List = listOf( - CommentLineAction(KT_EXTS, KT_LINE_COMMENT_TOKEN), - UncommentLineAction(KT_EXTS, KT_LINE_COMMENT_TOKEN) + CommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN), + UncommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN) ) } \ No newline at end of file From 429e500a0d92f06a80cac598b3a0bf071809e6cc Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 17 Apr 2026 01:00:20 +0530 Subject: [PATCH 54/58] fix: invalid label for uncomment line action Signed-off-by: Akash Yadav --- .../com/itsaky/androidide/lsp/actions/UncommentLineAction.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt index 78d632d557..66cf2b9ffd 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt @@ -61,7 +61,7 @@ class UncommentLineAction( } val context = data.requireContext() - label = context.getString(R.string.action_comment_line) + label = context.getString(R.string.action_uncomment_line) val file = data.requireFile() if (file.extension !in targetFileExtensions) { From ffd785ecdbe7aef30c480336fa1b5462d8833b81 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 17 Apr 2026 01:22:43 +0530 Subject: [PATCH 55/58] fix: collect and report Kotlin syntax errors Signed-off-by: Akash Yadav --- .../diagnostic/KotlinDiagnosticProvider.kt | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) 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 72a3ff2aba..54c3a70b1b 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 @@ -12,6 +12,10 @@ 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.PsiErrorElement +import org.jetbrains.kotlin.com.intellij.psi.PsiFile +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil import org.slf4j.LoggerFactory import java.nio.file.Path import kotlin.math.log @@ -44,9 +48,24 @@ private fun CompilationEnvironment.doAnalyze(file: Path): DiagnosticResult { } val diagnostics = project.read { - analyze(ktFile) { - ktFile.collectDiagnostics(KaDiagnosticCheckerFilter.EXTENDED_AND_COMMON_CHECKERS) - .map { it.toDiagnosticItem() } + buildList { + PsiTreeUtil.collectElementsOfType(ktFile, PsiErrorElement::class.java) + .forEach { errorElement -> + add( + diagnosticItem( + file = ktFile, + message = errorElement.errorDescription, + range = errorElement.textRange, + severity = DiagnosticSeverity.ERROR, + ) + ) + } + + analyze(ktFile) { + ktFile.collectDiagnostics(KaDiagnosticCheckerFilter.EXTENDED_AND_COMMON_CHECKERS) + .forEach { add(it.toDiagnosticItem()) } + } + } } @@ -59,17 +78,28 @@ private fun CompilationEnvironment.doAnalyze(file: Path): DiagnosticResult { } private fun KaDiagnosticWithPsi<*>.toDiagnosticItem(): DiagnosticItem { - val range = psi.textRange.toRange(psi.containingFile) val severity = severity.toDiagnosticSeverity() - return DiagnosticItem( + return diagnosticItem( + file = psi.containingFile, message = defaultMessage, - code = "", - range = range, - source = "Kotlin", + range = psi.textRange, severity = severity, ) } +private fun diagnosticItem( + file: PsiFile, + message: String, + range: TextRange, + severity: DiagnosticSeverity, +) = DiagnosticItem( + message = message, + code = "", + range = range.toRange(file), + source = "kotlin", + severity = severity, +) + private fun KaSeverity.toDiagnosticSeverity(): DiagnosticSeverity { return when (this) { KaSeverity.ERROR -> DiagnosticSeverity.ERROR From 7a9b0e9e182e6fa4de6903ea91fe9d83d920ea02 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 17 Apr 2026 21:13:25 +0530 Subject: [PATCH 56/58] feat: add 'Add import' action for kotlin source files Signed-off-by: Akash Yadav --- .../itsaky/androidide/actions/actionUtils.kt | 24 +- .../androidide/editor/ui/EditorActionsMenu.kt | 14 +- .../indexing/jvm/JvmSymbolIndex.kt | 6 + .../lsp/kotlin/KotlinCodeActionsMenu.kt | 4 +- .../lsp/kotlin/actions/AddImportAction.kt | 134 +++++ .../kotlin/actions/BaseKotlinCodeAction.kt | 60 +++ .../kotlin/compiler/index/KtSymbolIndex.kt | 5 +- .../diagnostic/KotlinDiagnosticProvider.kt | 13 +- .../androidide/lsp/kotlin/utils/EditExts.kt | 17 +- .../androidide/lsp/models/Completions.kt | 503 +++++++++--------- 10 files changed, 506 insertions(+), 274 deletions(-) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/BaseKotlinCodeAction.kt diff --git a/actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt b/actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt index cbe410ca5a..1254e58746 100644 --- a/actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt +++ b/actions/src/main/java/com/itsaky/androidide/actions/actionUtils.kt @@ -24,17 +24,23 @@ import io.github.rosemoe.sora.widget.CodeEditor import java.io.File import java.nio.file.Path -fun ActionData.getContext(): Context? { - return get(Context::class.java) -} +inline fun ActionData.get(): T?= + get(T::class.java) -fun ActionData.requireContext(): Context { - return getContext() ?: throw IllegalArgumentException("No context instance provided") -} +inline fun ActionData.require(): T = + checkNotNull(get()) -fun ActionData.requireFile(): File { - return get(File::class.java) ?: throw IllegalArgumentException("No file instance provided") -} +inline fun ActionData.has(): Boolean = + get() != null + +fun ActionData.getContext(): Context? = + get() + +fun ActionData.requireContext(): Context = + require() + +fun ActionData.requireFile(): File = + require() fun ActionData.requirePath(): Path { return requireFile().toPath() diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorActionsMenu.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorActionsMenu.kt index 9d12324f45..080f7b3a04 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorActionsMenu.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/EditorActionsMenu.kt @@ -46,8 +46,10 @@ import com.itsaky.androidide.editor.adapters.IdeEditorAdapter import com.itsaky.androidide.editor.databinding.LayoutPopupMenuItemBinding import com.itsaky.androidide.editor.ui.EditorActionsMenu.ActionsListAdapter.VH import com.itsaky.androidide.idetooltips.TooltipManager +import com.itsaky.androidide.lsp.api.ILanguageClient import com.itsaky.androidide.lsp.api.ILanguageServerRegistry import com.itsaky.androidide.lsp.java.JavaLanguageServer +import com.itsaky.androidide.lsp.kotlin.KotlinLanguageServer import com.itsaky.androidide.lsp.models.DiagnosticItem import com.itsaky.androidide.lsp.xml.XMLLanguageServer import com.itsaky.androidide.resources.R @@ -298,7 +300,9 @@ open class EditorActionsMenu(val editor: IDEEditor) : protected open fun onGetActionLocation() = location protected open fun onCreateActionData(): ActionData { + val languageServerRegistry = ILanguageServerRegistry.getDefault() val data = ActionData.create(editor.context) + data.put(IDEEditor::class.java, this.editor) data.put( CodeEditor::class.java, @@ -307,14 +311,18 @@ open class EditorActionsMenu(val editor: IDEEditor) : data.put(File::class.java, editor.file) data.put(DiagnosticItem::class.java, getDiagnosticAtCursor()) data.put(com.itsaky.androidide.models.Range::class.java, editor.cursorLSPRange) - data.put( + data.put( JavaLanguageServer::class.java, - ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID) + languageServerRegistry.getServer(JavaLanguageServer.SERVER_ID) as? JavaLanguageServer? ) + data.put(KotlinLanguageServer::class.java, + languageServerRegistry + .getServer(KotlinLanguageServer.SERVER_ID) + as? KotlinLanguageServer?) data.put( XMLLanguageServer::class.java, - ILanguageServerRegistry.getDefault().getServer(XMLLanguageServer.SERVER_ID) + languageServerRegistry.getServer(XMLLanguageServer.SERVER_ID) as? XMLLanguageServer? ) data.put(TextTarget::class.java, IdeEditorAdapter(this.editor)) 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 index 2b3c044e9c..75b9019ba6 100644 --- 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 @@ -126,6 +126,12 @@ class JvmSymbolIndex( this.limit = limit }) + fun findBySimpleName(name: String, limit: Int = 200) = + query(indexQuery { + eq(KEY_NAME, name) + this.limit = limit + }) + suspend fun findByKey(key: String): JvmSymbol? = get(key) fun allPackages(): Sequence = distinctValues(KEY_PACKAGE) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt index 4e451059a8..c021171b79 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt @@ -4,6 +4,7 @@ import com.itsaky.androidide.actions.ActionItem import com.itsaky.androidide.lsp.actions.CommentLineAction import com.itsaky.androidide.lsp.actions.IActionsMenuProvider import com.itsaky.androidide.lsp.actions.UncommentLineAction +import com.itsaky.androidide.lsp.kotlin.actions.AddImportAction object KotlinCodeActionsMenu : IActionsMenuProvider { @@ -14,6 +15,7 @@ object KotlinCodeActionsMenu : IActionsMenuProvider { override val actions: List = listOf( CommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN), - UncommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN) + UncommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN), + AddImportAction(), ) } \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt new file mode 100644 index 0000000000..5f5cc1b985 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt @@ -0,0 +1,134 @@ +package com.itsaky.androidide.lsp.kotlin.actions + +import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.actions.has +import com.itsaky.androidide.actions.markInvisible +import com.itsaky.androidide.actions.newDialogBuilder +import com.itsaky.androidide.actions.require +import com.itsaky.androidide.actions.requireFile +import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.lsp.kotlin.compiler.index.findSymbolBySimpleName +import com.itsaky.androidide.lsp.kotlin.diagnostic.KotlinDiagnosticExtra +import com.itsaky.androidide.lsp.kotlin.utils.insertImport +import com.itsaky.androidide.lsp.models.CodeActionItem +import com.itsaky.androidide.lsp.models.CodeActionKind +import com.itsaky.androidide.lsp.models.Command +import com.itsaky.androidide.lsp.models.DiagnosticItem +import com.itsaky.androidide.lsp.models.DocumentChange +import com.itsaky.androidide.lsp.models.TextEdit +import com.itsaky.androidide.resources.R +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol +import org.jetbrains.kotlin.analysis.api.fir.diagnostics.KaFirDiagnostic +import org.slf4j.LoggerFactory + +class AddImportAction : BaseKotlinCodeAction() { + override var titleTextRes: Int = R.string.action_import_classes + override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIX_IMPORTS + + override val id: String = "ide.editor.lsp.kt.diagnostics.addImport" + override var label: String = "" + + companion object { + private val logger = LoggerFactory.getLogger(AddImportAction::class.java) + } + + override fun prepare(data: ActionData) { + super.prepare(data) + + if (!visible || !data.has()) { + markInvisible() + return + } + + val extra = data.require().extra as? KotlinDiagnosticExtra + if (extra == null) { + markInvisible() + return + } + + val diagnostic = extra.diagnostic as? KaFirDiagnostic.UnresolvedReference? + if (diagnostic == null) { + markInvisible() + return + } + + val env = extra.compilationEnv + val reference = diagnostic.reference + val hasImportableSymbols = env.ktSymbolIndex + .findSymbolBySimpleName(reference, limit = 0) + .any { it.kind.isClassifier } + + if (!hasImportableSymbols) { + markInvisible() + return + } + } + + override suspend fun execAction(data: ActionData): Map> { + val (diagnostic, env) = data.require().extra as? KotlinDiagnosticExtra + ?: return emptyMap() + + diagnostic as KaFirDiagnostic.UnresolvedReference + + val file = data.requireFile() + val nioPath = file.toPath() + val ktFile = env.ktSymbolIndex + .getOpenedKtFile(nioPath) + ?: return emptyMap() + + return env.ktSymbolIndex + .findSymbolBySimpleName(diagnostic.reference, limit = 0) + .filter { it.kind.isClassifier } + .associateWith { symbol -> insertImport(ktFile, symbol.fqName) } + } + + override fun postExec(data: ActionData, result: Any) { + super.postExec(data, result) + + if (result !is Map<*, *>) { + return + } + + @Suppress("UNCHECKED_CAST") + result as Map> + + if (result.isEmpty()) { + logger.warn("No classifiers to import.") + return + } + + val client = data.languageClient + ?: run { + logger.warn("No language client set. Cannot complete action.") + return + } + + val file = data.requireFile() + val nioPath = file.toPath() + val actions = + result + .map { (symbol, edits) -> + CodeActionItem( + title = symbol.fqName, + changes = edits.map { DocumentChange(file = nioPath, edits = edits) }, + kind = CodeActionKind.QuickFix, + command = Command.CMD_FORMAT_CODE, + ) + } + + when (actions.size) { + 0 -> logger.error("No code actions found. Cannot completion action.") + 1 -> client.performCodeAction(actions[0]) + else -> newDialogBuilder(data) + .setTitle(label) + .setItems(actions.map { it.title }.toTypedArray()) { dialog, which -> + dialog.dismiss() + actions.getOrNull(which)?.also { client.performCodeAction(it) } + ?: run { + logger.error("Index $which is out of bounds for actions of size ${actions.size}") + } + } + .show() + } + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/BaseKotlinCodeAction.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/BaseKotlinCodeAction.kt new file mode 100644 index 0000000000..7706a4b49e --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/BaseKotlinCodeAction.kt @@ -0,0 +1,60 @@ +package com.itsaky.androidide.lsp.kotlin.actions + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.StringRes +import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.actions.ActionItem +import com.itsaky.androidide.actions.EditorActionItem +import com.itsaky.androidide.actions.get +import com.itsaky.androidide.actions.hasRequiredData +import com.itsaky.androidide.actions.markInvisible +import com.itsaky.androidide.actions.requireContext +import com.itsaky.androidide.actions.requireFile +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.lsp.kotlin.KotlinLanguageServer +import com.itsaky.androidide.utils.DocumentUtils +import org.slf4j.LoggerFactory +import java.io.File + +abstract class BaseKotlinCodeAction : EditorActionItem { + + override var visible: Boolean = true + override var enabled: Boolean = true + override var icon: Drawable? = null + override var requiresUIThread: Boolean = false + override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS + + @get:StringRes + protected abstract var titleTextRes: Int + + protected val logger = LoggerFactory.getLogger(BaseKotlinCodeAction::class.java) + + override fun prepare(data: ActionData) { + super.prepare(data) + if (!data.hasRequiredData( + Context::class.java, + KotlinLanguageServer::class.java, + File::class.java + ) + ) { + markInvisible() + return + } + + val context = data.requireContext() + val file = data.requireFile() + val isKtFile = DocumentUtils.isKotlinFile(file.toPath()) + + if (titleTextRes != -1) { + label = context.getString(titleTextRes) + } + + visible = isKtFile + enabled = isKtFile + } + + protected val ActionData.languageClient: ILanguageClient? + get() = get() + ?.client +} 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 index 822cc0c296..de7eaa3e74 100644 --- 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 @@ -138,4 +138,7 @@ internal fun KtSymbolIndex.filesForPackage(packageFqn: String) = fileIndex.getFilesForPackage(packageFqn) internal fun KtSymbolIndex.subpackageNames(packageFqn: String) = - fileIndex.getSubpackageNames(packageFqn) \ No newline at end of file + fileIndex.getSubpackageNames(packageFqn) + +internal fun KtSymbolIndex.findSymbolBySimpleName(name: String, limit: Int) = + sourceIndex.findBySimpleName(name, limit) + libraryIndex.findBySimpleName(name, limit) 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 54c3a70b1b..87b7460259 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 @@ -18,10 +18,14 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiFile import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil import org.slf4j.LoggerFactory import java.nio.file.Path -import kotlin.math.log private val logger = LoggerFactory.getLogger("KotlinDiagnosticProvider") +internal data class KotlinDiagnosticExtra( + val diagnostic: KaDiagnosticWithPsi<*>, + val compilationEnv: CompilationEnvironment, +) + internal fun CompilationEnvironment.collectDiagnosticsFor(file: Path): DiagnosticResult = try { logger.info("Analyzing file: {}", file) return doAnalyze(file) @@ -63,9 +67,12 @@ private fun CompilationEnvironment.doAnalyze(file: Path): DiagnosticResult { analyze(ktFile) { ktFile.collectDiagnostics(KaDiagnosticCheckerFilter.EXTENDED_AND_COMMON_CHECKERS) - .forEach { add(it.toDiagnosticItem()) } + .forEach { diagnostic -> + add(diagnostic.toDiagnosticItem().apply { + extra = KotlinDiagnosticExtra(diagnostic, this@doAnalyze) + }) + } } - } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt index 0c395847f4..0b86603991 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.PsiFile +import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory private val logger = LoggerFactory.getLogger("EditExts") @@ -32,8 +33,11 @@ fun TextRange.toRange(containingFile: PsiFile): Range { } context(ctx: AnalysisContext) -internal fun insertImport(fqn: String): List { - val imports = ctx.ktFile.importDirectives +internal fun insertImport(fqn: String): List = + insertImport(ctx.ktFile, fqn) + +internal fun insertImport(ktFile: KtFile, fqn: String): List { + val imports = ktFile.importDirectives val importText = "import $fqn" for (import in imports) { val thisFqn = import.importedFqName?.asString() ?: "" @@ -52,7 +56,7 @@ internal fun insertImport(fqn: String): List { return insertAfter(last, System.lineSeparator() + importText) } - ctx.ktFile.packageDirective?.also { pkg -> + ktFile.packageDirective?.also { pkg -> logger.info("insert {} after package stmt: {}", importText, pkg.text) return insertAfter(pkg, System.lineSeparator() + importText) } @@ -67,7 +71,6 @@ internal fun insertImport(fqn: String): List { ) } -context(ctx: AnalysisContext) internal fun insertBefore(element: PsiElement, text: String): List { val range = rangeOf(element) return listOf( @@ -78,7 +81,6 @@ internal fun insertBefore(element: PsiElement, text: String): List { ) } -context(ctx: AnalysisContext) internal fun insertAfter(element: PsiElement, text: String): List { val range = rangeOf(element) return listOf( @@ -89,7 +91,6 @@ internal fun insertAfter(element: PsiElement, text: String): List { ) } -context(ctx: AnalysisContext) -internal fun rangeOf(element: PsiElement): Range { - return element.textRange.toRange(ctx.ktFile) +internal fun rangeOf(element: PsiElement, containingFile: PsiFile = element.containingFile): Range { + return element.textRange.toRange(containingFile) } \ No newline at end of file diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Completions.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Completions.kt index dc64ee5796..1d460b5984 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Completions.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/models/Completions.kt @@ -41,281 +41,286 @@ import java.util.function.Consumer const val DEFAULT_MIN_MATCH_RATIO = 59 data class CompletionParams( - var position: Position, - var file: Path, - override val cancelChecker: ICancelChecker + var position: Position, + var file: Path, + override val cancelChecker: ICancelChecker ) : CancellableRequestParams { - var content: CharSequence? = null - var prefix: String? = null - - fun requirePrefix(): String { - if (prefix == null) { - throw IllegalArgumentException("Prefix is required but none was provided") - } - - return prefix as String - } - - fun requireContents(): CharSequence { - if (content == null) { - throw IllegalArgumentException("Content is required but no content was provided!") - } - return content as CharSequence - } + var content: CharSequence? = null + var prefix: String? = null + + fun requirePrefix(): String { + if (prefix == null) { + throw IllegalArgumentException("Prefix is required but none was provided") + } + + return prefix as String + } + + fun requireContents(): CharSequence { + if (content == null) { + throw IllegalArgumentException("Content is required but no content was provided!") + } + return content as CharSequence + } } open class CompletionResult(items: Collection) { - val items: List = run { - var temp = items.toMutableList() - temp.sort() - if (TRIM_TO_MAX && temp.size > MAX_ITEMS) { - temp = temp.subList(0, MAX_ITEMS) - } - return@run temp - } - - var isIncomplete = this.items.size < items.size - var isCached = false - - companion object { - const val MAX_ITEMS = 50 - @JvmField val EMPTY = CompletionResult(listOf()) - - var TRIM_TO_MAX = true - - @JvmStatic - @JvmOverloads - fun mapAndFilter( - src: CompletionResult, - partial: String, - map: Consumer = Consumer {} - ): CompletionResult { - val newItems = src.items.toMutableList() - newItems.forEach(map) - newItems.removeIf { !it.ideLabel.startsWith(partial) } - return CompletionResult(newItems) - } - } - - constructor() : this(listOf()) - - fun add(item: CompletionItem) { - if (isIncomplete) { - // Max limit has been reached - return - } - - if (items is MutableList) { - this.items.add(item) - } - this.isIncomplete = this.items.size >= MAX_ITEMS - } - - fun markCached() { - this.isCached = true - } - - override fun toString(): String { - return android.text.TextUtils.join("\n", items) - } + val items: List = run { + var temp = items.toMutableList() + temp.sort() + if (TRIM_TO_MAX && temp.size > MAX_ITEMS) { + temp = temp.subList(0, MAX_ITEMS) + } + return@run temp + } + + var isIncomplete = this.items.size < items.size + var isCached = false + + companion object { + const val MAX_ITEMS = 50 + @JvmField + val EMPTY = CompletionResult(listOf()) + + var TRIM_TO_MAX = true + + @JvmStatic + @JvmOverloads + fun mapAndFilter( + src: CompletionResult, + partial: String, + map: Consumer = Consumer {} + ): CompletionResult { + val newItems = src.items.toMutableList() + newItems.forEach(map) + newItems.removeIf { !it.ideLabel.startsWith(partial) } + return CompletionResult(newItems) + } + } + + constructor() : this(listOf()) + + fun add(item: CompletionItem) { + if (isIncomplete) { + // Max limit has been reached + return + } + + if (items is MutableList) { + this.items.add(item) + } + this.isIncomplete = this.items.size >= MAX_ITEMS + } + + fun markCached() { + this.isCached = true + } + + override fun toString(): String { + return android.text.TextUtils.join("\n", items) + } } open class CompletionItem( - var ideLabel: String, - var detail: String, - insertText: String?, - insertTextFormat: InsertTextFormat?, - sortText: String?, - var command: Command?, - var completionKind: CompletionItemKind, - var matchLevel: MatchLevel, - var additionalTextEdits: List?, - var data: ICompletionData?, - var editHandler: IEditHandler = DefaultEditHandler() + var ideLabel: String, + var detail: String, + insertText: String?, + insertTextFormat: InsertTextFormat?, + sortText: String?, + var command: Command?, + var completionKind: CompletionItemKind, + var matchLevel: MatchLevel, + var additionalTextEdits: List?, + var data: ICompletionData?, + var editHandler: IEditHandler = DefaultEditHandler() ) : - io.github.rosemoe.sora.lang.completion.CompletionItem(ideLabel, detail), Comparable { - - var ideSortText: String? = sortText - get() { - if (field == null) { - return ideLabel - } - - return field - } - - var insertText: String = insertText ?: "" - get() { - if (field.isEmpty()) { - return this.ideLabel - } - - return field - } - - var insertTextFormat: InsertTextFormat = insertTextFormat ?: PLAIN_TEXT - var additionalEditHandler: IEditHandler? = null - var snippetDescription: SnippetDescription? = null - var overrideTypeText: String? = null - - constructor() : - this( - "", // label - "", // detail - null, // insertText - null, // insertTextFormat - null, // sortText - null, // command - NONE, // kind - NO_MATCH, // match level - ArrayList(), // additionalEdits - null // data - ) - - companion object { - - @JvmStatic - @JvmOverloads - fun matchLevel( - candidate: String, - partial: String, - minMatchRatio: Int = DEFAULT_MIN_MATCH_RATIO - ): MatchLevel { - if (candidate.startsWith(partial)) { - return if (candidate.length == partial.length) { - CASE_SENSITIVE_EQUAL - } else { - CASE_SENSITIVE_PREFIX - } - } - - val lowerCandidate = candidate.lowercase() - val lowerPartial = partial.lowercase() - if (lowerCandidate.startsWith(lowerPartial)) { - return if (lowerCandidate.length == lowerPartial.length) { - CASE_INSENSITIVE_EQUAL - } else { - CASE_INSENSITIVE_PREFIX - } - } - - val ratio = FuzzySearch.ratio(candidate, partial) - if (ratio > minMatchRatio) { - return PARTIAL_MATCH - } - - return NO_MATCH - } - } - - override fun performCompletion(editor: CodeEditor, text: Content, position: CharPosition) { - editHandler.performEdits(this, editor, text, position.line, position.column, position.index) - } - - override fun performCompletion(editor: CodeEditor, text: Content, line: Int, column: Int) { - throw UnsupportedOperationException() - } - - override fun compareTo(other: CompletionItem): Int { - return CompletionItemComparator.compare(this, other) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is CompletionItem) return false - - if (ideLabel != other.ideLabel) return false - if (detail != other.detail) return false - if (command != other.command) return false - if (completionKind != other.completionKind) return false - if (matchLevel != other.matchLevel) return false - if (additionalTextEdits != other.additionalTextEdits) return false - if (data != other.data) return false - if (ideSortText != other.ideSortText) return false - if (insertText != other.insertText) return false - if (insertTextFormat != other.insertTextFormat) return false - if (editHandler != other.editHandler) return false - if (additionalEditHandler != other.additionalEditHandler) return false - if (overrideTypeText != other.overrideTypeText) return false - - return true - } - - override fun hashCode(): Int { - var result = ideLabel.hashCode() - result = 31 * result + detail.hashCode() - result = 31 * result + (command?.hashCode() ?: 0) - result = 31 * result + completionKind.hashCode() - result = 31 * result + matchLevel.hashCode() - result = 31 * result + (additionalTextEdits?.hashCode() ?: 0) - result = 31 * result + (data?.hashCode() ?: 0) - result = 31 * result + (ideSortText?.hashCode() ?: 0) - result = 31 * result + insertText.hashCode() - result = 31 * result + insertTextFormat.hashCode() - result = 31 * result + editHandler.hashCode() - result = 31 * result + (additionalEditHandler?.hashCode() ?: 0) - result = 31 * result + (overrideTypeText?.hashCode() ?: 0) - return result - } - - override fun toString(): String { - return "CompletionItem(label='$ideLabel', detail='$detail', command=$command, kind=$completionKind, matchLevel=$matchLevel, additionalTextEdits=$additionalTextEdits, data=$data, sortText=$ideSortText, insertText='$insertText', insertTextFormat=$insertTextFormat, editHandler=$editHandler, additionalEditHandler=$additionalEditHandler, overrideTypeText=$overrideTypeText)" - } + io.github.rosemoe.sora.lang.completion.CompletionItem(ideLabel, detail), + Comparable { + + var ideSortText: String? = sortText + get() { + if (field == null) { + return ideLabel + } + + return field + } + + var insertText: String = insertText ?: "" + get() { + if (field.isEmpty()) { + return this.ideLabel + } + + return field + } + + var insertTextFormat: InsertTextFormat = insertTextFormat ?: PLAIN_TEXT + var additionalEditHandler: IEditHandler? = null + var snippetDescription: SnippetDescription? = null + var overrideTypeText: String? = null + + constructor() : + this( + "", // label + "", // detail + null, // insertText + null, // insertTextFormat + null, // sortText + null, // command + NONE, // kind + NO_MATCH, // match level + ArrayList(), // additionalEdits + null // data + ) + + companion object { + + @JvmStatic + @JvmOverloads + fun matchLevel( + candidate: String, + partial: String, + minMatchRatio: Int = DEFAULT_MIN_MATCH_RATIO + ): MatchLevel { + if (candidate.startsWith(partial)) { + return if (candidate.length == partial.length) { + CASE_SENSITIVE_EQUAL + } else { + CASE_SENSITIVE_PREFIX + } + } + + val lowerCandidate = candidate.lowercase() + val lowerPartial = partial.lowercase() + if (lowerCandidate.startsWith(lowerPartial)) { + return if (lowerCandidate.length == lowerPartial.length) { + CASE_INSENSITIVE_EQUAL + } else { + CASE_INSENSITIVE_PREFIX + } + } + + val ratio = FuzzySearch.ratio(candidate, partial) + if (ratio > minMatchRatio) { + return PARTIAL_MATCH + } + + return NO_MATCH + } + } + + override fun performCompletion(editor: CodeEditor, text: Content, position: CharPosition) { + editHandler.performEdits(this, editor, text, position.line, position.column, position.index) + } + + override fun performCompletion(editor: CodeEditor, text: Content, line: Int, column: Int) { + throw UnsupportedOperationException() + } + + override fun compareTo(other: CompletionItem): Int { + return CompletionItemComparator.compare(this, other) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CompletionItem) return false + + if (ideLabel != other.ideLabel) return false + if (detail != other.detail) return false + if (command != other.command) return false + if (completionKind != other.completionKind) return false + if (matchLevel != other.matchLevel) return false + if (additionalTextEdits != other.additionalTextEdits) return false + if (data != other.data) return false + if (ideSortText != other.ideSortText) return false + if (insertText != other.insertText) return false + if (insertTextFormat != other.insertTextFormat) return false + if (editHandler != other.editHandler) return false + if (additionalEditHandler != other.additionalEditHandler) return false + if (overrideTypeText != other.overrideTypeText) return false + + return true + } + + override fun hashCode(): Int { + var result = ideLabel.hashCode() + result = 31 * result + detail.hashCode() + result = 31 * result + (command?.hashCode() ?: 0) + result = 31 * result + completionKind.hashCode() + result = 31 * result + matchLevel.hashCode() + result = 31 * result + (additionalTextEdits?.hashCode() ?: 0) + result = 31 * result + (data?.hashCode() ?: 0) + result = 31 * result + (ideSortText?.hashCode() ?: 0) + result = 31 * result + insertText.hashCode() + result = 31 * result + insertTextFormat.hashCode() + result = 31 * result + editHandler.hashCode() + result = 31 * result + (additionalEditHandler?.hashCode() ?: 0) + result = 31 * result + (overrideTypeText?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "CompletionItem(label='$ideLabel', detail='$detail', command=$command, kind=$completionKind, matchLevel=$matchLevel, additionalTextEdits=$additionalTextEdits, data=$data, sortText=$ideSortText, insertText='$insertText', insertTextFormat=$insertTextFormat, editHandler=$editHandler, additionalEditHandler=$additionalEditHandler, overrideTypeText=$overrideTypeText)" + } } data class SnippetDescription @JvmOverloads constructor( - val selectedLength: Int, - val deleteSelected: Boolean = true, - val snippet: CodeSnippet? = null, - val allowCommandExecution: Boolean = false + val selectedLength: Int, + val deleteSelected: Boolean = true, + val snippet: CodeSnippet? = null, + val allowCommandExecution: Boolean = false ) data class Command(var title: String, var command: String) { - companion object { + companion object { - /** Action for triggering a signature help request to the language server. */ - const val TRIGGER_PARAMETER_HINTS = "editor.action.triggerParameterHints" + /** Action for triggering a signature help request to the language server. */ + const val TRIGGER_PARAMETER_HINTS = "editor.action.triggerParameterHints" + val CMD_TRIGGER_PARAMETER_HINTS = Command("Trigger parameter hints", TRIGGER_PARAMETER_HINTS) - /** Action for triggering a completion request to the language server. */ - const val TRIGGER_COMPLETION = "editor.action.triggerCompletionRequest" + /** Action for triggering a completion request to the language server. */ + const val TRIGGER_COMPLETION = "editor.action.triggerCompletionRequest" + val CMD_TRIGGER_COMPLETION = Command("Trigger completion", TRIGGER_COMPLETION) - /** Action for triggering code format action automatically. */ - const val FORMAT_CODE = "editor.action.formatCode" - } + /** Action for triggering code format action automatically. */ + const val FORMAT_CODE = "editor.action.formatCode" + val CMD_FORMAT_CODE = Command("Format code", FORMAT_CODE) + } } enum class CompletionItemKind { - KEYWORD, - VARIABLE, - PROPERTY, - FIELD, - ENUM_MEMBER, - CONSTRUCTOR, - METHOD, - FUNCTION, - TYPE_PARAMETER, - CLASS, - INTERFACE, - ENUM, - ANNOTATION_TYPE, - MODULE, - SNIPPET, - VALUE, - NONE + KEYWORD, + VARIABLE, + PROPERTY, + FIELD, + ENUM_MEMBER, + CONSTRUCTOR, + METHOD, + FUNCTION, + TYPE_PARAMETER, + CLASS, + INTERFACE, + ENUM, + ANNOTATION_TYPE, + MODULE, + SNIPPET, + VALUE, + NONE } enum class MatchLevel { - CASE_SENSITIVE_EQUAL, - CASE_INSENSITIVE_EQUAL, - CASE_SENSITIVE_PREFIX, - CASE_INSENSITIVE_PREFIX, - PARTIAL_MATCH, - NO_MATCH + CASE_SENSITIVE_EQUAL, + CASE_INSENSITIVE_EQUAL, + CASE_SENSITIVE_PREFIX, + CASE_INSENSITIVE_PREFIX, + PARTIAL_MATCH, + NO_MATCH } enum class InsertTextFormat { - PLAIN_TEXT, - SNIPPET + PLAIN_TEXT, + SNIPPET } From 3fdbe67dc7f5cd664ff8f4c5373726ccf6e07338 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sat, 18 Apr 2026 14:59:04 +0530 Subject: [PATCH 57/58] fix: un-imported extension symbols are not shown in completion items Signed-off-by: Akash Yadav --- .../codeonthego/indexing/jvm/JvmSymbol.kt | 6 ++- .../indexing/jvm/KotlinMetadataScanner.kt | 8 +++- .../kotlin/compiler/CompilationEnvironment.kt | 3 +- .../lsp/kotlin/compiler/index/IndexWorker.kt | 1 + .../compiler/index/SourceFileIndexer.kt | 7 +++- .../completion/AdvancedKotlinEditHandler.kt | 6 ++- ...dler.kt => KotlinAutoImportEditHandler.kt} | 20 +++++---- .../kotlin/completion/KotlinCompletions.kt | 41 ++++++++++++++----- .../lsp/edits/DefaultEditHandler.kt | 19 +++++---- 9 files changed, 78 insertions(+), 33 deletions(-) rename lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/{KotlinClassImportEditHandler.kt => KotlinAutoImportEditHandler.kt} (59%) 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 4e6cb46bac..aec88b542e 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 @@ -84,6 +84,9 @@ data class JvmSymbol( else -> null } + val receiverTypeFqName: String? + get() = receiverTypeName?.toFqName() + val containingClassName: String get() = data.containingClassName @@ -237,4 +240,5 @@ data class JvmTypeAliasInfo( private fun String.toFqName() = replace('/', '.') - .replace('$', '.') \ No newline at end of file + .replace('$', '.') + .replace('#', '.') \ No newline at end of file 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 ab6d9c7f46..8c11a91c54 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 @@ -260,6 +260,7 @@ object KotlinMetadataScanner { val receiverType = fn.receiverParameterType val isExtension = receiverType != null + val receiverTypeDisplayName = receiverType?.let { kmTypeToDisplayName(it) } ?: "" val kind = if (isExtension) JvmSymbolKind.EXTENSION_FUNCTION else JvmSymbolKind.FUNCTION val parameters = fn.valueParameters.map { param -> @@ -277,6 +278,11 @@ object KotlinMetadataScanner { val key = "$name(${parameters.joinToString(",") { it.typeFqName }})" val signatureDisplay = buildString { + if (isExtension) { + append(receiverTypeDisplayName) + append('.') + } + append("(") append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplayName}" }) append("): ") @@ -302,7 +308,7 @@ object KotlinMetadataScanner { typeParameters = fn.typeParameters.map { it.name }, kotlin = KotlinFunctionInfo( receiverTypeName = receiverType?.let { kmTypeToName(it) } ?: "", - receiverTypeDisplayName = receiverType?.let { kmTypeToDisplayName(it) } ?: "", + receiverTypeDisplayName = receiverTypeDisplayName, isSuspend = fn.isSuspend, isInline = fn.isInline, isInfix = fn.isInfix, 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 3c59cdf099..70e7019c29 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 @@ -87,8 +87,6 @@ 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 /** @@ -388,6 +386,7 @@ internal class CompilationEnvironment( provider.registerInMemoryFile(path.pathString, newKtFile.virtualFile) ktSymbolIndex.openKtFile(path, newKtFile) + ktSymbolIndex.queueOnFileChangedAsync(newKtFile) project.write { KaSourceModificationService.getInstance(project) .handleElementModification(newKtFile, KaElementModificationType.Unknown) 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 index 3be16fdecc..60f55f8634 100644 --- 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 @@ -46,6 +46,7 @@ internal class IndexWorker( } is IndexCommand.IndexModifiedFile -> { + logger.debug("Indexing modified ktFile: {}", command.ktFile) indexSourceFile(project, command.ktFile, fileIndex, sourceIndex) sourceIndexCount++ } 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 index 95eaeb533f..3e8a131882 100644 --- 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 @@ -1,6 +1,9 @@ package com.itsaky.androidide.lsp.kotlin.compiler.index +import com.itsaky.androidide.lsp.kotlin.compiler.modules.backingFilePath import com.itsaky.androidide.lsp.kotlin.compiler.read +import com.itsaky.androidide.lsp.kotlin.utils.toNioPathOrNull +import com.itsaky.androidide.projects.FileManager import org.appdevforall.codeonthego.indexing.jvm.JvmClassInfo import org.appdevforall.codeonthego.indexing.jvm.JvmFieldInfo import org.appdevforall.codeonthego.indexing.jvm.JvmFunctionInfo @@ -45,9 +48,9 @@ import kotlin.io.path.pathString internal fun KtFile.toMetadata(project: Project, isIndexed: Boolean = false): KtFileMetadata = project.read { KtFileMetadata( - filePath = virtualFile.toNioPath().pathString, + filePath = (virtualFile.toNioPathOrNull() ?: backingFilePath)!!.pathString, packageFqName = packageFqName.asString(), - lastModified = Instant.ofEpochMilli(virtualFile.timeStamp), + lastModified = (backingFilePath?.let { FileManager.getLastModified(it) }) ?: Instant.ofEpochMilli(virtualFile.timeStamp), modificationStamp = modificationStamp, isIndexed = isIndexed, symbolKeys = emptyList() 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 d3a32d82d6..08090c0bda 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 @@ -29,12 +29,16 @@ internal abstract class AdvancedKotlinEditHandler( return } - performEdits(managedFile, editor, item) + context(analysisContext) { + performEdits(managedFile, editor, item) + } + if (item.command != null) { executeCommand(editor, item.command) } } + context(ctx: AnalysisContext) abstract fun performEdits( ktFile: KtFile, editor: CodeEditor, 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/KotlinAutoImportEditHandler.kt similarity index 59% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt rename to lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinAutoImportEditHandler.kt index 66d3b3fb93..0b568fc04b 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/KotlinAutoImportEditHandler.kt @@ -6,22 +6,28 @@ 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.appdevforall.codeonthego.indexing.jvm.JvmSymbol import org.jetbrains.kotlin.psi.KtFile -internal class KotlinClassImportEditHandler( +internal class KotlinAutoImportEditHandler( analysisContext: AnalysisContext, + private val symbolToImport: JvmSymbol? = null, ) : AdvancedKotlinEditHandler(analysisContext) { + + context(ctx: AnalysisContext) override fun performEdits( ktFile: KtFile, editor: CodeEditor, item: CompletionItem ) { - val data = item.data as? ClassCompletionData ?: return - context(analysisContext) { - val edits = insertImport(data.className) - if (edits.isNotEmpty()) { - RewriteHelper.performEdits(edits, editor) - } + val fqnToImport = + symbolToImport?.fqName + ?: (item.data as? ClassCompletionData)?.className + ?: return + + val edits = insertImport(ktFile, fqnToImport) + if (edits.isNotEmpty()) { + RewriteHelper.performEdits(edits, editor) } } } \ No newline at end of file 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 dd92d771b3..142fab4f2c 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 @@ -16,6 +16,7 @@ import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.InsertTextFormat import com.itsaky.androidide.preferences.utils.indentationString +import com.itsaky.androidide.project.ProjectInfo import com.itsaky.androidide.projects.FileManager import kotlinx.coroutines.CancellationException import org.appdevforall.codeonthego.indexing.jvm.JvmClassInfo @@ -289,7 +290,7 @@ context(env: CompilationEnvironment, ctx: AnalysisContext) private fun KaSession.collectUnimportedSymbols( to: MutableList ) { - val currentPackage = ctx.ktElement.containingKtFile.packageDirective?.name + val currentPackage = ctx.ktElement.containingKtFile.packageDirective?.fqName?.asString() val useSiteModule = this.useSiteModule // Library symbols: JAR-based, use full SymbolVisibilityChecker @@ -361,18 +362,31 @@ private fun KaSession.buildUnimportedSymbolItem(symbol: JvmSymbol): CompletionIt item.overrideTypeText = symbol.returnTypeDisplay when (symbol.kind) { - JvmSymbolKind.FUNCTION, JvmSymbolKind.CONSTRUCTOR -> { + JvmSymbolKind.EXTENSION_FUNCTION, JvmSymbolKind.FUNCTION, JvmSymbolKind.CONSTRUCTOR -> { val data = symbol.data as JvmFunctionInfo item.detail = data.signatureDisplay item.setInsertTextForFunction( name = symbol.shortName, hasParams = data.parameterCount > 0, ) + + item.additionalEditHandler = KotlinAutoImportEditHandler( + analysisContext = ctx, + symbolToImport = symbol + ) + if (symbol.kind == JvmSymbolKind.CONSTRUCTOR) { item.overrideTypeText = symbol.shortName } } + in JvmSymbolKind.CALLABLE_KINDS -> { + item.additionalEditHandler = KotlinAutoImportEditHandler( + analysisContext = ctx, + symbolToImport = symbol + ) + } + JvmSymbolKind.TYPE_ALIAS -> { item.detail = (symbol.data as JvmTypeAliasInfo).expandedTypeFqName } @@ -390,7 +404,6 @@ private fun KaSession.buildUnimportedSymbolItem(symbol: JvmSymbol): CompletionIt else -> {} } - logger.debug("Adding completion item: {}", item) return item } @@ -399,7 +412,7 @@ private fun internalNameToClassId(internalName: String): ClassId { val packageName = internalName.substringBeforeLast('/') val relativeName = internalName.substringAfterLast('/') return ClassId( - packageFqName = FqName.fromSegments(packageName.split('.')), + packageFqName = FqName.fromSegments(packageName.split('/')), relativeClassName = FqName.fromSegments(relativeName.split('$')), isLocal = isLocal ) @@ -433,10 +446,10 @@ private fun KaSession.collectSnippetCompletions(to: MutableList) KotlinSnippetRepository.snippets[KotlinSnippetScope.GLOBAL]?.also { addAll(it) } val snippetScope = when (ctx.declarationKind) { - DeclarationKind.CLASS , - DeclarationKind.INTERFACE , - DeclarationKind.OBJECT , - DeclarationKind.ENUM_CLASS , + DeclarationKind.CLASS, + DeclarationKind.INTERFACE, + DeclarationKind.OBJECT, + DeclarationKind.ENUM_CLASS, DeclarationKind.ANNOTATION_CLASS -> KotlinSnippetScope.MEMBER DeclarationKind.CONSTRUCTOR, @@ -449,7 +462,12 @@ private fun KaSession.collectSnippetCompletions(to: MutableList) DeclarationKind.TYPEALIAS -> null } - logger.info("Adding completions for snippet scope: {} (context: {}, kind: {})", snippetScope, ctx.declarationContext, ctx.declarationKind) + logger.info( + "Adding completions for snippet scope: {} (context: {}, kind: {})", + snippetScope, + ctx.declarationContext, + ctx.declarationKind + ) KotlinSnippetRepository.snippets[snippetScope]?.also { addAll(it) } } @@ -478,7 +496,8 @@ private fun computeIndentLevelAt(ktElement: KtElement): Int { if (current is KtBlockExpression || current is KtClassBody || current is KtWhenExpression || - current is KtFunction) { + current is KtFunction + ) { indentLevel++ } current = current.parent @@ -601,7 +620,7 @@ private fun CompletionItem.setClassCompletionData( topLevelClass ) - additionalEditHandler = KotlinClassImportEditHandler(analysisContext = ctx) + additionalEditHandler = KotlinAutoImportEditHandler(analysisContext = ctx) } context(ctx: AnalysisContext) diff --git a/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt b/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt index fb49916024..19375860e1 100644 --- a/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt +++ b/lsp/models/src/main/java/com/itsaky/androidide/lsp/edits/DefaultEditHandler.kt @@ -25,6 +25,7 @@ import com.itsaky.androidide.lsp.models.InsertTextFormat.SNIPPET import com.itsaky.androidide.lsp.util.RewriteHelper import io.github.rosemoe.sora.lang.completion.snippet.parser.CodeSnippetParser import io.github.rosemoe.sora.text.Content +import io.github.rosemoe.sora.text.batchEdit import io.github.rosemoe.sora.widget.CodeEditor import org.slf4j.LoggerFactory @@ -82,17 +83,17 @@ open class DefaultEditHandler : IEditHandler { text.delete(line, start, line, column) editor.commitText(item.insertText) - text.beginBatchEdit() - if (item.additionalEditHandler != null) { - item.additionalEditHandler!!.performEdits(item, editor, text, line, column, index) - } else if (item.additionalTextEdits != null && item.additionalTextEdits!!.isNotEmpty()) { - RewriteHelper.performEdits(item.additionalTextEdits!!, editor) - } - text.beginBatchEdit() - + performAdditionalEdits(item, editor, text, line, column, index) executeCommand(editor, item.command) } + protected open fun performAdditionalEdits(item: CompletionItem, editor: CodeEditor, text: Content, line: Int, column: Int, index: Int) { + text.batchEdit { + item.additionalEditHandler?.performEdits(item, editor, text, line, column, index) + ?: item.additionalTextEdits?.also { RewriteHelper.performEdits(it, editor) } + } + } + protected open fun insertSnippet( item: CompletionItem, editor: CodeEditor, @@ -112,6 +113,8 @@ open class DefaultEditHandler : IEditHandler { } editor.snippetController.startSnippet(actionIndex, snippet, selectedText) + performAdditionalEdits(item, editor, text, line, column, index) + if (snippetDescription.allowCommandExecution) { executeCommand(editor, item.command) } From c255c52f82447e1efcee33c1120234739148fcd0 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 21 Apr 2026 23:27:56 +0530 Subject: [PATCH 58/58] fix: remove unused class Signed-off-by: Akash Yadav --- .../KotlinClassImportEditHandler.kt | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt 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 deleted file mode 100644 index 66d3b3fb93..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.completion - -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( - ktFile: KtFile, - editor: CodeEditor, - item: CompletionItem - ) { - val data = item.data as? ClassCompletionData ?: return - context(analysisContext) { - val edits = insertImport(data.className) - if (edits.isNotEmpty()) { - RewriteHelper.performEdits(edits, editor) - } - } - } -} \ No newline at end of file