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 e2d50f51d3..c2da35bdcd 100755 --- a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt +++ b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt @@ -46,7 +46,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.plus -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,9 +102,6 @@ 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/FilteredIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndex.kt index a110f276dd..740a0ae8a7 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 @@ -30,7 +30,7 @@ open class FilteredIndex( /** * Make a source's entries visible in query results. */ - fun activateSource(sourceId: String) { + open fun activateSource(sourceId: String) { activeSources.add(sourceId) } @@ -38,7 +38,7 @@ open class FilteredIndex( * Hide a source's entries from query results. * The data remains in the backing index. */ - fun deactivateSource(sourceId: String) { + open fun deactivateSource(sourceId: String) { activeSources.remove(sourceId) } @@ -49,7 +49,7 @@ open class FilteredIndex( * This is the typical call on project sync: pass in all * current classpath JAR paths. */ - fun setActiveSources(sourceIds: Set) { + open fun setActiveSources(sourceIds: Set) { activeSources.clear() activeSources.addAll(sourceIds) } @@ -57,13 +57,13 @@ open class FilteredIndex( /** * Returns the current set of active source IDs. */ - fun activeSources(): Set = + open fun activeSources(): Set = activeSources.toSet() /** * Returns true if the source is currently active (visible). */ - fun isActive(sourceId: String): Boolean = + open fun isActive(sourceId: String): Boolean = sourceId in activeSources /** @@ -72,24 +72,24 @@ open class FilteredIndex( * * Use this to check if a JAR needs indexing at all. */ - suspend fun isCached(sourceId: String): Boolean = + open suspend fun isCached(sourceId: String): Boolean = backing.containsSource(sourceId) override fun query(query: IndexQuery): Sequence { - if (query.sourceId != null && query.sourceId !in activeSources) { + if (query.sourceId != null && !isActive(query.sourceId)) { return emptySequence() } val original = backing.query(query) - return original.filter { it.sourceId in activeSources } + return original.filter { isActive(it.sourceId) } } override suspend fun get(key: String): T? { val entry = backing.get(key) ?: return null - return if (entry.sourceId in activeSources) entry else null + return if (isActive(entry.sourceId)) entry else null } override suspend fun containsSource(sourceId: String): Boolean { - return sourceId in activeSources && backing.containsSource(sourceId) + return isActive(sourceId) && backing.containsSource(sourceId) } override fun distinctValues(fieldName: String): Sequence { 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 bccfb6f69d..e130b72608 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 @@ -126,15 +126,32 @@ class InMemoryIndex( } 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() + val buckets = prefixBuckets[field] + val matching: Set = if (buckets != null) { + // Prefix-searchable: case-insensitive match via the lowercased buckets, + // mirroring SQLite's `lowerCol LIKE 'prefix%'`. + val lowerPrefix = prefix.lowercase() + val firstChar = lowerPrefix.firstOrNull() + if (firstChar == null) { + // Empty prefix == "field present", matching SQLite's `LIKE '%'` + // (which excludes rows where the column IS NULL). + buckets.values.flatMapTo(mutableSetOf()) { entries -> entries.map { it.key } } + } else { + val bucket = buckets[firstChar] ?: return@read emptySequence() + bucket.asSequence() + .filter { it.lowerValue.startsWith(lowerPrefix) } + .map { it.key } + .toSet() + } + } else { + // Not prefix-searchable: fall back to a case-sensitive prefix scan of the + // regular field map, mirroring SQLite's `col LIKE 'prefix%'` fallback. + val fieldMap = fieldMaps[field] ?: return@read emptySequence() + fieldMap.entries.asSequence() + .filter { (value, _) -> value.startsWith(prefix) } + .flatMap { (_, keys) -> keys.asSequence() } + .toSet() + } candidates = intersect(candidates, matching) } 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 53dd8463ac..bac69aee0b 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 @@ -20,7 +20,7 @@ import java.io.Closeable /** * An index of symbols from JVM source and binary files. */ -class JvmSymbolIndex( +open class JvmSymbolIndex( private val backing: Index, private val indexer: BackgroundIndexer, ) : FilteredIndex(backing), WritableIndex by backing, Closeable { 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 index e70c112cf9..e1112ac1c1 100644 --- 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 @@ -2,6 +2,7 @@ package org.appdevforall.codeonthego.indexing.jvm import android.content.Context import org.appdevforall.codeonthego.indexing.SQLiteIndex +import org.appdevforall.codeonthego.indexing.api.Index 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 @@ -10,19 +11,19 @@ import java.io.Closeable /** * An index of [KtFileMetadata] entries, one per Kotlin source file. */ -class KtFileMetadataIndex private constructor( - private val backing: SQLiteIndex, +class KtFileMetadataIndex( + private val backing: Index, ) : Closeable { companion object { /** - * Creates a [KtFileMetadataIndex] backed by an in-memory SQLite database. + * Creates a [KtFileMetadataIndex] backed by a 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( + fun sqliteBacked( context: Context, dbName: String? = null ): KtFileMetadataIndex = 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 a10d66a52e..bc38e6bfef 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 @@ -144,7 +144,7 @@ class KotlinLanguageServer : ILanguageServer { indexingRegistry.register( key = KT_SOURCE_FILE_META_INDEX_KEY, - index = KtFileMetadataIndex.create( + index = KtFileMetadataIndex.sqliteBacked( context = context, dbName = KT_SOURCE_FILE_META_INDEX_KEY.name ) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt new file mode 100644 index 0000000000..dce87b311f --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt @@ -0,0 +1,327 @@ +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.isSourceModule +import com.itsaky.androidide.lsp.kotlin.compiler.registrar.AnalysisApiServiceProvider +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.WriteAccessGuard +import com.itsaky.androidide.lsp.kotlin.compiler.services.latestLanguageVersionSettings +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.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.packages.KotlinPackagePartProviderFactory +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderFactory +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.ApplicationServiceRegistration +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.StandaloneProjectFactory +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectExtensionPoints +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectModelServices +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectServices +import org.jetbrains.kotlin.cli.common.intellijPluginRoot +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.CliMetadataFinderFactory +import org.jetbrains.kotlin.cli.jvm.compiler.CliVirtualFileFinderFactory +import org.jetbrains.kotlin.cli.jvm.compiler.JvmPackagePartProvider +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCliJavaFileManagerImpl +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironmentMode +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment +import org.jetbrains.kotlin.cli.jvm.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.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.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.vfs.VirtualFileManager +import org.jetbrains.kotlin.com.intellij.psi.ClassTypePointerFactory +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 +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.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.KtPsiFactory +import java.nio.file.Path +import kotlin.io.path.pathString + +/** + * Base class shared by [CompilationEnvironment] (production) and the test-only + * `KtLspTestEnvironment`. Handles all IntelliJ / Analysis API infrastructure + * that is identical in both environments: + */ +@OptIn(K1Deprecation::class) +internal abstract class AbstractCompilationEnvironment( + val name: String, + val kind: CompilationKind, + val intellijPluginRoot: Path, + val jdkHome: Path, + val jdkRelease: Int, + val languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, + val applicationEnvironmentMode: KotlinCoreApplicationEnvironmentMode, + val enableParserEventSystem: Boolean = true, +) : AutoCloseable { + + companion object { + init { + System.setProperty("java.awt.headless", "true") + setupIdeaStandaloneExecution() + } + } + + protected val disposable = Disposer.newDisposable("CompilationEnvironment[$name]") + + lateinit var projectEnv: KotlinCoreProjectEnvironment + val applicationEnv: KotlinCoreApplicationEnvironment + get() = projectEnv.environment as KotlinCoreApplicationEnvironment + val application: MockApplication + get() = applicationEnv.application + val project: MockProject + get() = projectEnv.project + + lateinit var modules: List + lateinit var libraryRoots: List + lateinit var ktSymbolIndex: KtSymbolIndex + lateinit var parser: KtPsiFactory + + val psiManager: PsiManager + get() = PsiManager.getInstance(project) + + protected abstract fun createServiceRegistrars(): List + + /** + * Wires platform services to [ktSymbolIndex], [modules], and [libraryRoots] + * via [KtLspService.setupWith]. The default implementation calls [KtLspService.setupWith] + * for all standard Analysis API services. + */ + protected open fun setupServices(libraryRoots: List) { + listOf( + KotlinModuleDependentsProvider::class.java, + KotlinProjectStructureProvider::class.java, + KotlinPackageProviderFactory::class.java, + KotlinDeclarationProviderFactory::class.java, + KotlinPackagePartProviderFactory::class.java, + KotlinAnnotationsResolverFactory::class.java, + KotlinDirectInheritorsProvider::class.java, + ).forEach { svcClass -> + (project.getService(svcClass) as KtLspService).setupWith( + project = project, + index = ktSymbolIndex, + modules = modules, + libraryRoots = libraryRoots, + ) + } + } + + /** Called at the end of [initialize]. Production uses this to start background indexing. */ + protected open fun postInit(libraryRoots: List) {} + + /** The [MessageCollector] used by the [CompilerConfiguration]. Defaults to no-op. */ + protected open fun createMessageCollector(): MessageCollector = object : MessageCollector { + override fun clear() {} + override fun hasErrors() = false + override fun report( + severity: CompilerMessageSeverity, + message: String, + location: CompilerMessageSourceLocation?, + ) { + } + } + + @Suppress("UnstableApiUsage") + protected open fun initialize( + buildModules: ( + project: MockProject, + applicationEnv: KotlinCoreApplicationEnvironment, + ) -> List, + buildKtSymbolIndex: ( + modules: List, + libraryRoots: List, + ) -> KtSymbolIndex, + ) { + projectEnv = StandaloneProjectFactory.createProjectEnvironment( + projectDisposable = disposable, + applicationEnvironmentMode = applicationEnvironmentMode, + compilerConfiguration = createCompilerConfiguration(), + ) + + project.registerRWLock() + + val serviceRegistrars = createServiceRegistrars() + ApplicationServiceRegistration.registerWithCustomRegistration( + application, + serviceRegistrars, + ) { + registerApplicationServices(application, data = Unit) + } + + KotlinCoreEnvironment.registerProjectExtensionPoints(project.extensionArea) + + val appExtArea = application.extensionArea + CoreApplicationEnvironment.registerExtensionPoint( + appExtArea, + ClassTypePointerFactory.EP_NAME, + ClassTypePointerFactory::class.java, + ) + + appExtArea.getExtensionPoint(ClassTypePointerFactory.EP_NAME) + .registerExtension(PsiClassReferenceTypePointerFactory(), application) + + CoreApplicationEnvironment.registerExtensionPoint( + appExtArea, + DocumentWriteAccessGuard.EP_NAME, + WriteAccessGuard::class.java, + ) + + modules = buildModules(project, applicationEnv) + + serviceRegistrars.registerProjectExtensionPoints(project, data = Unit) + serviceRegistrars.registerProjectServices(project, data = Unit) + serviceRegistrars.registerProjectModelServices(project, disposable, data = Unit) + + + libraryRoots = modules + .asFlatSequence() + .filterNot { it.isSourceModule } + .flatMap { lib -> + lib.computeFiles(extended = false).map { JavaRoot(it, JavaRoot.RootType.BINARY) } + } + .toList() + + ktSymbolIndex = buildKtSymbolIndex(modules, libraryRoots) + + val librariesScope = ProjectScope.getLibrariesScope(project) + 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 (javaSourceRoots, singleJavaFileRoots) = modules + .asFlatSequence() + .filter { it.isSourceModule } + .flatMap { it.contentRoots } + .mapNotNull { VirtualFileManager.getInstance().findFileByNioPath(it) } + .partition { it.isDirectory || it.extension != JavaFileType.DEFAULT_EXTENSION } + + val rootsIndex = + JvmDependenciesDynamicCompoundIndex(shouldOnlyFindFirstClass = true).apply { + addIndex( + JvmDependenciesIndexImpl( + libraryRoots + javaSourceRoots.map { + JavaRoot( + it, + JavaRoot.RootType.SOURCE + ) + }, + shouldOnlyFindFirstClass = true, + ) + ) + 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) + } + } + } + } + + javaFileManager.initialize( + index = rootsIndex, + packagePartProviders = listOf(packagePartProvider), + singleJavaFileRootsIndex = SingleJavaFileRootsIndex( + singleJavaFileRoots.map { JavaRoot(it, JavaRoot.RootType.SOURCE) } + ), + usePsiClassFilesReading = true, + perfManager = null, + ) + + val fileFinderFactory = CliVirtualFileFinderFactory(rootsIndex, false, perfManager = null) + 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) + ) + } + + setupServices(libraryRoots) + + parser = KtPsiFactory(project, eventSystemEnabled = enableParserEventSystem) + + postInit(libraryRoots) + } + + private fun createCompilerConfiguration(): CompilerConfiguration = + CompilerConfiguration().apply { + this.moduleName = JvmProtoBufUtil.DEFAULT_MODULE_NAME + this.useFir = true + this.intellijPluginRoot = + this@AbstractCompilationEnvironment.intellijPluginRoot.pathString + this.languageVersionSettings = LanguageVersionSettingsImpl( + languageVersion = this@AbstractCompilationEnvironment.languageVersion, + apiVersion = ApiVersion.createByLanguageVersion(this@AbstractCompilationEnvironment.languageVersion), + analysisFlags = emptyMap(), + specificFeatures = LanguageFeature.entries.associateWith { LanguageFeature.State.ENABLED }, + ) + this.jdkHome = this@AbstractCompilationEnvironment.jdkHome.toFile() + this.jdkRelease = this@AbstractCompilationEnvironment.jdkRelease + this.messageCollector = createMessageCollector() + } + + override fun close() { + Disposer.dispose(disposable) + } +} 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 395a41fefb..92c01e5d73 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 @@ -6,15 +6,10 @@ import com.itsaky.androidide.lsp.kotlin.compiler.modules.AbstractKtModule import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule import com.itsaky.androidide.lsp.kotlin.compiler.modules.asFlatSequence import com.itsaky.androidide.lsp.kotlin.compiler.modules.backingFilePath -import com.itsaky.androidide.lsp.kotlin.compiler.modules.isSourceModule -import com.itsaky.androidide.lsp.kotlin.compiler.registrar.LspServiceRegistrar -import com.itsaky.androidide.lsp.kotlin.compiler.services.JavaModuleAccessibilityChecker -import com.itsaky.androidide.lsp.kotlin.compiler.services.JavaModuleAnnotationsProvider -import com.itsaky.androidide.lsp.kotlin.compiler.services.KtLspService +import com.itsaky.androidide.lsp.kotlin.compiler.registrar.AnalysisApiServiceProviders +import com.itsaky.androidide.lsp.kotlin.compiler.registrar.LspAnalysisApiServiceRegistrar import com.itsaky.androidide.lsp.kotlin.compiler.services.ProjectStructureProvider import com.itsaky.androidide.lsp.kotlin.compiler.services.ResolutionScopeProvider -import com.itsaky.androidide.lsp.kotlin.compiler.services.WriteAccessGuard -import com.itsaky.androidide.lsp.kotlin.compiler.services.latestLanguageVersionSettings import com.itsaky.androidide.lsp.kotlin.diagnostic.collectDiagnosticsFor import com.itsaky.androidide.lsp.kotlin.utils.SymbolVisibilityChecker import com.itsaky.androidide.lsp.kotlin.utils.toVirtualFileOrNull @@ -29,80 +24,24 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.withContext 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.KaImplementationDetail import org.jetbrains.kotlin.analysis.api.platform.analysisMessageBus -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.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.modification.KotlinModuleStateModificationEvent import org.jetbrains.kotlin.analysis.api.platform.modification.KotlinModuleStateModificationKind import org.jetbrains.kotlin.analysis.api.platform.modification.publishModificationEvent -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.projectStructure.KotlinModuleDependentsProvider -import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinProjectStructureProvider -import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.ApplicationServiceRegistration -import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.StandaloneProjectFactory -import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectExtensionPoints -import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectModelServices -import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectServices import org.jetbrains.kotlin.analysis.low.level.api.fir.sessions.LLFirSessionInvalidationTopics -import org.jetbrains.kotlin.cli.common.intellijPluginRoot import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation import org.jetbrains.kotlin.cli.common.messages.MessageCollector -import org.jetbrains.kotlin.cli.jvm.compiler.CliMetadataFinderFactory -import org.jetbrains.kotlin.cli.jvm.compiler.CliVirtualFileFinderFactory -import org.jetbrains.kotlin.cli.jvm.compiler.JvmPackagePartProvider -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCliJavaFileManagerImpl import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironmentMode -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment import org.jetbrains.kotlin.cli.jvm.index.JavaRoot -import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesDynamicCompoundIndex -import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndexImpl -import org.jetbrains.kotlin.cli.jvm.index.SingleJavaFileRootsIndex -import org.jetbrains.kotlin.cli.jvm.modules.CliJavaModuleFinder -import org.jetbrains.kotlin.cli.jvm.modules.CliJavaModuleResolver -import org.jetbrains.kotlin.cli.jvm.modules.JavaModuleGraph -import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment -import org.jetbrains.kotlin.com.intellij.core.CorePackageIndex -import org.jetbrains.kotlin.com.intellij.ide.highlighter.JavaFileType -import org.jetbrains.kotlin.com.intellij.mock.MockApplication import org.jetbrains.kotlin.com.intellij.mock.MockProject -import org.jetbrains.kotlin.com.intellij.openapi.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.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 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.messageCollector -import org.jetbrains.kotlin.config.moduleName -import org.jetbrains.kotlin.config.useFir -import org.jetbrains.kotlin.load.kotlin.MetadataFinderFactory -import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory -import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtPsiFactory import org.slf4j.LoggerFactory import java.nio.file.Path import kotlin.io.path.pathString @@ -111,49 +50,43 @@ import kotlin.time.Duration.Companion.milliseconds /** * 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 intellijPluginRoot The IntelliJ plugin root. This is usually the embeddable JAR location. Required. + * @param languageVersion The language version this environment should target. * @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 name: String, - val kind: CompilationKind, - workspace: Workspace, + name: String, + kind: CompilationKind, + private val workspace: Workspace, val ktProject: KotlinProjectModel, - val intellijPluginRoot: Path, - val jdkHome: Path, - val jdkRelease: Int, - val languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, - val enableParserEventSystem: Boolean = true, - val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + CoroutineName("CompilationEnv[$name]")) -) : KotlinProjectModel.ProjectModelListener, AutoCloseable { - private var disposable = Disposer.newDisposable() - private var _languageClient: ILanguageClient? = null - - val fileAnalyzer: KeyedDebouncingAction - val projectEnv: KotlinCoreProjectEnvironment - - val applicationEnv: KotlinCoreApplicationEnvironment - get() = projectEnv.environment as KotlinCoreApplicationEnvironment - - val application: MockApplication - get() = applicationEnv.application + intellijPluginRoot: Path, + jdkHome: Path, + jdkRelease: Int, + languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, + enableParserEventSystem: Boolean = true, + val coroutineScope: CoroutineScope = CoroutineScope( + SupervisorJob() + CoroutineName("CompilationEnv[$name]") + ), +) : AbstractCompilationEnvironment( + name = name, + kind = kind, + intellijPluginRoot = intellijPluginRoot, + jdkHome = jdkHome, + jdkRelease = jdkRelease, + languageVersion = languageVersion, + applicationEnvironmentMode = KotlinCoreApplicationEnvironmentMode.Production, + enableParserEventSystem = enableParserEventSystem, +), KotlinProjectModel.ProjectModelListener { - val project: MockProject - get() = projectEnv.project - - val parser: KtPsiFactory - val commandProcessor: CommandProcessor - val modules: List + companion object { + val DEFAULT_FILE_MOD_EVENT_DEBOUNCE_DURATION = 400.milliseconds + private val logger = LoggerFactory.getLogger(CompilationEnvironment::class.java) + } - val psiManager: PsiManager - get() = PsiManager.getInstance(project) + private var _languageClient: ILanguageClient? = null - val psiDocumentManager: PsiDocumentManager - get() = PsiDocumentManager.getInstance(project) + val fileAnalyzer: KeyedDebouncingAction val libraryIndex: JvmSymbolIndex? get() = ktProject.libraryIndex @@ -177,8 +110,7 @@ internal class CompilationEnvironment( get() = ktProject.generatedIndex val symbolVisibilityChecker: SymbolVisibilityChecker by lazy { - val provider = ProjectStructureProvider.getInstance(project) - SymbolVisibilityChecker(provider) + SymbolVisibilityChecker(ProjectStructureProvider.getInstance(project)) } var languageClient: ILanguageClient? @@ -187,247 +119,74 @@ internal class CompilationEnvironment( _languageClient = value } - val ktSymbolIndex by lazy { - KtSymbolIndex( - kind = kind, - project = project, - modules = modules, - fileIndex = requireFileIndex, - sourceIndex = requireSourceIndex, - libraryIndex = requireLibraryIndex, - ) + init { + initialize(::buildModules, ::buildKtSymbolIndex) } - private val serviceRegistrars = listOf(LspServiceRegistrar) - - private val envMessageCollector = object : MessageCollector { - override fun clear() { - } - + @OptIn(KaImplementationDetail::class) + @Suppress("UNUSED_PARAMETER") + private fun buildKtSymbolIndex( + modules: List, + libraryRoots: List, + ): KtSymbolIndex = KtSymbolIndex( + kind = kind, + project = project, + modules = modules, + fileIndex = requireFileIndex, + sourceIndex = requireSourceIndex, + libraryIndex = requireLibraryIndex, + ) + + private fun buildModules( + project: MockProject, + applicationEnv: KotlinCoreApplicationEnvironment, + ): List = workspace.collectKtModules(project, applicationEnv) + + override fun createServiceRegistrars() = + listOf(LspAnalysisApiServiceRegistrar(AnalysisApiServiceProviders.Production)) + + override fun createMessageCollector(): MessageCollector = object : MessageCollector { + override fun clear() {} + override fun hasErrors() = false override fun report( severity: CompilerMessageSeverity, message: String, - location: CompilerMessageSourceLocation? + location: CompilerMessageSourceLocation?, ) { logger.info("[{}] {} ({})", severity.name, message, location) } - - override fun hasErrors(): Boolean { - return false - } - } - companion object { - - val DEFAULT_FILE_MOD_EVENT_DEBOUNCE_DURATION = 400.milliseconds - - private val logger = LoggerFactory.getLogger(CompilationEnvironment::class.java) + override fun postInit(libraryRoots: List) { + ktSymbolIndex.syncIndexInBackground() } init { - - projectEnv = StandaloneProjectFactory - .createProjectEnvironment( - projectDisposable = disposable, - applicationEnvironmentMode = KotlinCoreApplicationEnvironmentMode.Production, - compilerConfiguration = createCompilerConfiguration(), - ) - - project.registerRWLock() - - ApplicationServiceRegistration.registerWithCustomRegistration( - application, - serviceRegistrars, - ) { - registerApplicationServices(application, data = Unit) - } - - KotlinCoreEnvironment.registerProjectExtensionPoints(project.extensionArea) - - CoreApplicationEnvironment.registerExtensionPoint( - application.extensionArea, - ClassTypePointerFactory.EP_NAME, - ClassTypePointerFactory::class.java, - ) - - application.extensionArea.getExtensionPoint(ClassTypePointerFactory.EP_NAME) - .registerExtension(PsiClassReferenceTypePointerFactory(), application) - - CoreApplicationEnvironment.registerExtensionPoint( - application.extensionArea, - DocumentWriteAccessGuard.EP_NAME, - WriteAccessGuard::class.java, - ) - - serviceRegistrars.registerProjectExtensionPoints(project, data = Unit) - serviceRegistrars.registerProjectServices(project, data = Unit) - serviceRegistrars.registerProjectModelServices(project, disposable, data = Unit) - - modules = workspace.collectKtModules(project, applicationEnv) - - val librariesScope = ProjectScope.getLibrariesScope(project) - val libraryRoots = modules - .asFlatSequence() - .filterNot { it.isSourceModule } - .flatMap { libMod -> - libMod.computeFiles(extended = false) - .map { file -> JavaRoot(file, JavaRoot.RootType.BINARY) } - } - .toList() - - val javaFileManager = - project.getService(JavaFileManager::class.java) as KotlinCliJavaFileManagerImpl - val javaModuleFinder = - CliJavaModuleFinder(jdkHome.toFile(), null, javaFileManager, project, jdkRelease) - val javaModuleGraph = JavaModuleGraph(javaModuleFinder) - val delegateJavaModuleResolver = - CliJavaModuleResolver(javaModuleGraph, emptyList(), emptyList(), project) - - val corePackageIndex = project.getService(PackageIndex::class.java) as CorePackageIndex - val packagePartProvider = JvmPackagePartProvider( - latestLanguageVersionSettings, - librariesScope - ).apply { - addRoots(libraryRoots, MessageCollector.NONE) - } - - val (javaRoots, singleJavaFileRoots) = modules - .asFlatSequence() - .filter { it.isSourceModule } - .flatMap { it.contentRoots } - .mapNotNull { VirtualFileManager.getInstance().findFileByNioPath(it) } - .partition { it.isDirectory || it.extension != JavaFileType.DEFAULT_EXTENSION } - - val rootsIndex = - JvmDependenciesDynamicCompoundIndex(shouldOnlyFindFirstClass = true).apply { - addIndex( - JvmDependenciesIndexImpl( - libraryRoots + javaRoots.map { JavaRoot(it, JavaRoot.RootType.SOURCE) }, - shouldOnlyFindFirstClass = true - ) - ) - - 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) - } - } - } - } - - javaFileManager.initialize( - index = rootsIndex, - packagePartProviders = listOf(packagePartProvider), - singleJavaFileRootsIndex = SingleJavaFileRootsIndex(singleJavaFileRoots.map { - JavaRoot( - it, - JavaRoot.RootType.SOURCE - ) - }), - usePsiClassFilesReading = true, - perfManager = null, - ) - - val fileFinderFactory = CliVirtualFileFinderFactory(rootsIndex, false, perfManager = null) - - with(project) { - registerService( - KotlinJavaModuleAccessibilityChecker::class.java, - JavaModuleAccessibilityChecker(delegateJavaModuleResolver) - ) - registerService( - KotlinJavaModuleAnnotationsProvider::class.java, - JavaModuleAnnotationsProvider(delegateJavaModuleResolver), - ) - registerService(VirtualFileFinderFactory::class.java, fileFinderFactory) - registerService( - MetadataFinderFactory::class.java, - CliMetadataFinderFactory(fileFinderFactory) - ) - } - - // Setup platform services - val lspServices = listOf( - KotlinModuleDependentsProvider::class.java, - KotlinProjectStructureProvider::class.java, - KotlinPackageProviderFactory::class.java, - KotlinDeclarationProviderFactory::class.java, - KotlinPackagePartProviderFactory::class.java, - KotlinAnnotationsResolverFactory::class.java, - KotlinDirectInheritorsProvider::class.java, - ) - - for (lspService in lspServices) { - (project.getService(lspService) as KtLspService).setupWith( - project = project, - index = ktSymbolIndex, - modules = modules, - libraryRoots = libraryRoots - ) - } - - commandProcessor = application.getService(CommandProcessor::class.java) - parser = KtPsiFactory(project, eventSystemEnabled = enableParserEventSystem) - fileAnalyzer = KeyedDebouncingAction( scope = coroutineScope, - debounceDuration = DEFAULT_FILE_MOD_EVENT_DEBOUNCE_DURATION + debounceDuration = DEFAULT_FILE_MOD_EVENT_DEBOUNCE_DURATION, ) { path, cancelChecker -> val result = collectDiagnosticsFor(path, cancelChecker) - withContext(Dispatchers.Main.immediate) { languageClient?.publishDiagnostics(result) } } - - // Sync the index in the background - ktSymbolIndex.syncIndexInBackground() - } - - private fun createCompilerConfiguration(): CompilerConfiguration { - return CompilerConfiguration().apply { - this.moduleName = JvmProtoBufUtil.DEFAULT_MODULE_NAME - this.useFir = true - this.intellijPluginRoot = this@CompilationEnvironment.intellijPluginRoot.pathString - this.languageVersionSettings = LanguageVersionSettingsImpl( - languageVersion = this@CompilationEnvironment.languageVersion, - apiVersion = ApiVersion.createByLanguageVersion(this@CompilationEnvironment.languageVersion), - analysisFlags = emptyMap(), - specificFeatures = LanguageFeature.entries.associateWith { LanguageFeature.State.ENABLED } - ) - - this.jdkHome = this@CompilationEnvironment.jdkHome.toFile() - this.jdkRelease = this@CompilationEnvironment.jdkRelease - - this.messageCollector = this@CompilationEnvironment.envMessageCollector - } } fun refreshSources() { Sentry.addBreadcrumb("refreshSources (env=${name}, modules=${modules.size})") project.write { Sentry.addBreadcrumb("refreshSources(env=${name}): in-progress") - ResolutionScopeProvider.getInstance(project) - .invalidateAll() - + ResolutionScopeProvider.getInstance(project).invalidateAll() modules.asFlatSequence() .filterIsInstance() .forEach { it.invalidateSearchScope() } } - ktSymbolIndex.refreshSources() - // TODO: Should also update/notify Java file services about possibly changed Java files - // But that's a bit problematic right now, scheduled for later } fun openFileIfNeeded(path: Path) { - ktSymbolIndex.getOpenedKtFile(path) - ?: onFileOpen(path) + ktSymbolIndex.getOpenedKtFile(path) ?: onFileOpen(path) } fun onFileOpen(path: Path) { @@ -443,14 +202,13 @@ internal class CompilationEnvironment( fun onFileClosed(path: Path) { fileAnalyzer.cancelPending(path) ktSymbolIndex.closeKtFile(path) - ProjectStructureProvider.getInstance(project) - .unregisterInMemoryFile(path.pathString) + ProjectStructureProvider.getInstance(project).unregisterInMemoryFile(path.pathString) } @OptIn(KaImplementationDetail::class) private inline fun notifyElementModifiedForPath( path: Path, - typeProvider: (KtFile) -> KaElementModificationType + typeProvider: (KtFile) -> KaElementModificationType, ) { val structureProvider = ProjectStructureProvider.getInstance(project) val ktFile = path.toVirtualFileOrNull()?.let { @@ -471,31 +229,24 @@ internal class CompilationEnvironment( project.publishModificationEvent( KotlinModuleStateModificationEvent( module, - KotlinModuleStateModificationKind.UPDATE + KotlinModuleStateModificationKind.UPDATE, ) ) - project.analysisMessageBus .syncPublisher(LLFirSessionInvalidationTopics.SESSION_INVALIDATION) .afterInvalidation(setOf(module)) - - ResolutionScopeProvider.getInstance(project) - .invalidate(module) + ResolutionScopeProvider.getInstance(project).invalidate(module) } else { project.analysisMessageBus .syncPublisher(LLFirSessionInvalidationTopics.SESSION_INVALIDATION) .afterGlobalInvalidation() - - - ResolutionScopeProvider.getInstance(project) - .invalidateAll() + ResolutionScopeProvider.getInstance(project).invalidateAll() } } } suspend fun onFileCreated(path: Path) { notifyElementModifiedForPath(path) { KaElementModificationType.ElementAdded } - ktSymbolIndex.submitForIndexing(path) } @@ -503,19 +254,14 @@ internal class CompilationEnvironment( notifyElementModifiedForPath(path) { ktFile -> KaElementModificationType.ElementRemoved(ktFile) } - - ProjectStructureProvider.getInstance(project) - .unregisterInMemoryFile(path.pathString) - + ProjectStructureProvider.getInstance(project).unregisterInMemoryFile(path.pathString) ktSymbolIndex.removeFromIndex(path) } suspend fun onFileMoved(fromPath: Path, toPath: Path) { val isFileOpen = ktSymbolIndex.getOpenedKtFile(fromPath) != null - onFileRemoved(fromPath) onFileCreated(toPath) - if (isFileOpen) { ktSymbolIndex.closeKtFile(fromPath) onFileOpen(toPath) @@ -531,7 +277,6 @@ internal class CompilationEnvironment( newKtFile.backingFilePath = path - // Tell ProjectStructureProvider which module owns this LightVirtualFile. val provider = ProjectStructureProvider.getInstance(project) provider.registerInMemoryFile(path.pathString, newKtFile.virtualFile) @@ -539,7 +284,6 @@ internal class CompilationEnvironment( val toInvalidate = oldKtFile ?: newKtFile KaSourceModificationService.getInstance(project) .handleElementModification(toInvalidate, KaElementModificationType.Unknown) - ktSymbolIndex.openKtFile(path, newKtFile) ktSymbolIndex.queueOnFileChangedAsync(newKtFile) fileAnalyzer.schedule(path) @@ -554,12 +298,11 @@ internal class CompilationEnvironment( override fun close() { ktProject.removeListener(this) - disposable.dispose() + super.close() } override fun onProjectModelChanged( model: KotlinProjectModel, - changeKind: KotlinProjectModel.ChangeKind - ) { - } -} \ No newline at end of file + changeKind: KotlinProjectModel.ChangeKind, + ) = Unit +} 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 eb5f4e859e..6d38dee820 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 @@ -204,6 +204,7 @@ private fun KaSession.kaTypeDisplayName(type: KaType): String { } private fun KaSession.analyzeFunction(filePath: String, dcl: KtNamedFunction): JvmSymbol? { + if (dcl.isLocal) return null val fnName = dcl.name ?: return null val visibility = dcl.jvmVisibility() if (visibility == JvmVisibility.PRIVATE) return null @@ -338,6 +339,7 @@ private fun KaSession.analyzeClassOrObject(filePath: String, dcl: KtClassOrObjec } private fun KaSession.analyzeProperty(filePath: String, dcl: KtProperty): JvmSymbol? { + if (dcl.isLocal) return null val propName = dcl.name ?: return null val visibility = dcl.jvmVisibility() if (visibility == JvmVisibility.PRIVATE) return null 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 index 03b5fb86a7..9c1f603ef0 100644 --- 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 @@ -24,12 +24,24 @@ internal abstract class AbstractKtModule( return } - val files = computeFiles(extended = true).toList() - _baseSearchScope = GlobalSearchScope.filesScope(project, files) + _baseSearchScope = computeBaseContentScope() _contentScope = KaContentScopeProvider.getInstance(project).getRefinedContentScope(this) Sentry.addBreadcrumb("createSearchScopes(mod=$this, base=${_baseSearchScope?.hashCode()}, content=${_contentScope?.hashCode()})") } + /** + * Computes the base content scope for this module. + * + * The default is a snapshot of the currently resolvable [VirtualFile]s + * ([computeFiles]). Subclasses whose membership is better expressed by path + * (e.g. source modules, where files are created/deleted/refreshed while + * editing) should override this with a path-predicate scope so that scope + * membership stays consistent with the way the module is resolved in + * `ProjectStructureProvider.getModule`. + */ + protected open fun computeBaseContentScope(): GlobalSearchScope = + GlobalSearchScope.filesScope(project, computeFiles(extended = true).toList()) + fun invalidateSearchScope() { synchronized(searchScopeLock) { Sentry.addBreadcrumb("invalidateSearchScope(mod=$this)") diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt new file mode 100644 index 0000000000..7ffef26605 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt @@ -0,0 +1,58 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.modules + +import com.itsaky.androidide.lsp.kotlin.compiler.read +import com.itsaky.androidide.lsp.kotlin.compiler.services.ProjectStructureProvider +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.VirtualFile +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager +import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope +import kotlin.io.path.extension +import kotlin.io.path.isDirectory +import kotlin.io.path.walk + +internal abstract class AbstractSourceModule( + project: Project, + directRegularDependencies: List +) : AbstractKtModule(project, directRegularDependencies) { + + 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) + } + } + + /** + * Membership is decided by path rather than by a frozen snapshot of + * [VirtualFile] instances. This keeps the content scope consistent with + * [ProjectStructureProvider.getModule], which resolves files to this module + * by path (`findModuleForSourceId`). + * + * A snapshot-based scope (`filesScope`) goes stale whenever a source file is + * created, or its [VirtualFile] instance changes due to a VFS refresh (e.g. + * right after a build). The file is then still mapped to this module by path + * but is absent from the scope, which makes the Analysis API reject it with + * `KaBaseIllegalPsiException` ("element cannot be analyzed in the context of + * the current session"). The predicate below mirrors the [computeFiles] + * filter and `findModuleForSourceId`, so the two can never disagree. + */ + override fun computeBaseContentScope(): GlobalSearchScope = + object : GlobalSearchScope(project) { + override fun contains(file: VirtualFile): Boolean { + if (file.fileSystem.protocol != "file") return false + val ext = file.extension + if (ext != "kt" && ext != "java") return false + val path = runCatching { file.toNioPath() }.getOrNull() ?: return false + return contentRoots.any { root -> path == root || path.startsWith(root) } + } + + override fun isSearchInModuleContent(aModule: Module): Boolean = true + + override fun isSearchInLibraries(): Boolean = false + } +} \ 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 index 254d863574..cb62f1cf90 100644 --- 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 @@ -2,14 +2,11 @@ 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.KaExperimentalApi 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 @@ -18,16 +15,13 @@ import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl import org.jetbrains.kotlin.platform.TargetPlatform import org.jetbrains.kotlin.platform.jvm.JvmPlatforms import org.slf4j.LoggerFactory -import kotlin.io.path.extension -import kotlin.io.path.isDirectory -import kotlin.io.path.walk @OptIn(KaPlatformInterface::class) internal class KtSourceModule( project: Project, val module: ModuleProject, directRegularDependencies: List, -) : KaSourceModule, AbstractKtModule(project, directRegularDependencies) { +) : KaSourceModule, AbstractSourceModule(project, directRegularDependencies) { companion object { private val logger = LoggerFactory.getLogger(KtSourceModule::class.java) @@ -82,7 +76,7 @@ internal class KtSourceModule( @OptIn(KaExperimentalApi::class) override val moduleDescription: String - get() = super.moduleDescription + get() = super.moduleDescription override val languageVersionSettings: LanguageVersionSettings get() = LanguageVersionSettingsImpl( @@ -93,16 +87,6 @@ internal class KtSourceModule( 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) - } - } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt new file mode 100644 index 0000000000..b3f7c0a841 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt @@ -0,0 +1,135 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.registrar + +import org.jetbrains.kotlin.com.intellij.mock.MockComponentManager +import kotlin.reflect.KClass + +internal typealias ServiceMap = Map, ServiceRegistration<*>> +internal typealias MutableServiceMap = MutableMap, ServiceRegistration<*>> + +internal sealed class ServiceRegistration { + + companion object { + operator fun invoke(klass: KClass, type: KClass) = + Typed(klass, type) + + operator fun invoke(klass: KClass, instance: T) = + Instance(klass, instance) + + operator fun invoke(klass: KClass, factory: () -> T) = + InstanceFactory(klass, factory) + } + + abstract val klass: KClass + + abstract fun register(to: MockComponentManager) + + data class Typed(override val klass: KClass, val type: KClass) : + ServiceRegistration() { + override fun register(to: MockComponentManager) { + to.registerService(klass.java, type.java) + } + } + + data class Instance(override val klass: KClass, val instance: T) : + ServiceRegistration() { + override fun register(to: MockComponentManager) { + to.registerService(klass.java, instance) + } + } + + data class InstanceFactory(override val klass: KClass, val factory: () -> T) : + ServiceRegistration() { + override fun register(to: MockComponentManager) { + val instance = factory() + to.registerService(klass.java, instance) + } + } +} + +internal interface AnalysisApiServiceProvider { + val pluginRelativePath: String? + + val applicationServices: ServiceMap + val projectServices: ServiceMap + + fun toBuilder(): Builder = Builder( + pluginRelativePath = pluginRelativePath, + appServices = applicationServices, + projectServices = projectServices, + ) + + private data class SimpleAnalysisApiServiceProvider( + override val pluginRelativePath: String?, + override val applicationServices: ServiceMap, + override val projectServices: ServiceMap, + ) : AnalysisApiServiceProvider + + class Builder( + var pluginRelativePath: String? = null, + appServices: ServiceMap = emptyMap(), + projectServices: ServiceMap = emptyMap(), + ) { + private val applicationServices: MutableServiceMap = mutableMapOf() + private val projectServices: MutableServiceMap = mutableMapOf() + + init { + appServices.forEach { (klass, either) -> appSvc(klass, either, false) } + projectServices.forEach { (klass, either) -> projSvc(klass, either, false) } + } + + private fun put( + store: MutableServiceMap, + key: KClass<*>, + value: ServiceRegistration<*>, + replace: Boolean + ) { + if (!replace) { + check(store.putIfAbsent(key, value) == null) { + "Service $key already registered" + } + } else { + check(store.replace(key, value) != null) { + "Service $key not found" + } + } + } + + private fun appSvc( + key: KClass<*>, value: ServiceRegistration<*>, replace: Boolean = false + ) = put(applicationServices, key, value, replace) + + private fun projSvc( + key: KClass<*>, value: ServiceRegistration<*>, replace: Boolean = false + ) = put(projectServices, key, value, replace) + + fun appService( + key: KClass, + type: KClass = key, + replace: Boolean = false, + ) = appSvc(key, ServiceRegistration(key, type), replace) + + fun appService(key: KClass, value: T, replace: Boolean = false) = + appSvc(key, ServiceRegistration(key, value), replace) + + fun appService(key: KClass, replace: Boolean = false, value: () -> T) = + appSvc(key, ServiceRegistration(key, value), replace) + + fun projectService( + key: KClass, + type: KClass = key, + replace: Boolean = false, + ) = projSvc(key, ServiceRegistration(key, type), replace) + + fun projectService(key: KClass, value: T, replace: Boolean = false) = + projSvc(key, ServiceRegistration(key, value), replace) + + fun projectService(key: KClass, replace: Boolean = false, value: () -> T) = + projSvc(key, ServiceRegistration(key, value), replace) + + fun build(): AnalysisApiServiceProvider = SimpleAnalysisApiServiceProvider( + pluginRelativePath = pluginRelativePath, + applicationServices = applicationServices.toMap(), + projectServices = projectServices.toMap(), + ) + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt new file mode 100644 index 0000000000..c115caf114 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt @@ -0,0 +1,62 @@ +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.NoOpAsyncExecutionService +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.com.intellij.openapi.application.AsyncExecutionService +import org.jetbrains.kotlin.com.intellij.psi.SmartTypePointerManager +import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartTypePointerManagerImpl + +@Suppress("UnstableApiUsage") +@OptIn(KaImplementationDetail::class) +internal object AnalysisApiServiceProviders { + + val BaseProvider = AnalysisApiServiceProvider.Builder().apply { + pluginRelativePath = "/META-INF/kt-lsp/kt-lsp.xml" + + appService(KotlinAnalysisPermissionOptions::class, AnalysisPermissionOptions::class) + appService(AsyncExecutionService::class, NoOpAsyncExecutionService::class) + + projectService( + KotlinLifetimeTokenFactory::class, + KotlinReadActionConfinementLifetimeTokenFactory::class + ) + + projectService(KotlinPlatformSettings::class, PlatformSettings::class) + projectService(SmartTypePointerManager::class, SmartTypePointerManagerImpl::class) + projectService(KotlinProjectStructureProvider::class, ProjectStructureProvider::class) + projectService(KotlinModuleDependentsProvider::class, ModuleDependentsProvider::class) + projectService(KotlinModificationTrackerFactory::class, ModificationTrackerFactory::class) + projectService(KotlinAnnotationsResolverFactory::class, AnnotationsResolverFactory::class) + projectService(KotlinDeclarationProviderMerger::class, DeclarationProviderMerger::class) + projectService(KotlinPackageProviderMerger::class, PackageProviderMerger::class) + projectService(KotlinPackagePartProviderFactory::class, PackagePartProviderFactory::class) + projectService(KotlinPackageProviderFactory::class, PackageProviderFactory::class) + projectService(KotlinDeclarationProviderFactory::class, DeclarationProviderFactory::class) + }.build() + + val Production = BaseProvider +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt new file mode 100644 index 0000000000..92454ac27d --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt @@ -0,0 +1,48 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.registrar + +import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.AnalysisApiSimpleServiceRegistrar +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.PluginStructureProvider +import org.jetbrains.kotlin.asJava.finder.JavaElementFinder +import org.jetbrains.kotlin.com.intellij.mock.MockApplication +import org.jetbrains.kotlin.com.intellij.mock.MockComponentManager +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.Disposable +import org.jetbrains.kotlin.com.intellij.psi.PsiElementFinder +import org.jetbrains.kotlin.com.intellij.psi.impl.PsiElementFinderImpl + +@OptIn(KaImplementationDetail::class) +internal class LspAnalysisApiServiceRegistrar( + private val provider: AnalysisApiServiceProvider, +): AnalysisApiSimpleServiceRegistrar() { + + @Suppress("UNCHECKED_CAST") + private fun MockComponentManager.servAll(services: ServiceMap) { + services.forEach { (_, registration) -> + registration.register(this) + } + } + + override fun registerApplicationServices(application: MockApplication) { + provider.pluginRelativePath?.also { pluginRelativePath -> + PluginStructureProvider.registerApplicationServices(application, pluginRelativePath) + } + + application.servAll(provider.applicationServices) + } + + override fun registerProjectServices(project: MockProject) { + provider.pluginRelativePath?.also { pluginRelativePath -> + PluginStructureProvider.registerProjectServices(project, pluginRelativePath) + } + + project.servAll(provider.projectServices) + } + + override fun registerProjectModelServices(project: MockProject, disposable: Disposable) { + with(PsiElementFinder.EP.getPoint(project)) { + registerExtension(JavaElementFinder(project), disposable) + registerExtension(PsiElementFinderImpl(project), disposable) + } + } +} \ 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 deleted file mode 100644 index f3b0edbfa3..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspServiceRegistrar.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.compiler.registrar - -import com.itsaky.androidide.lsp.kotlin.compiler.services.AnalysisPermissionOptions -import com.itsaky.androidide.lsp.kotlin.compiler.services.AnnotationsResolverFactory -import com.itsaky.androidide.lsp.kotlin.compiler.services.NoOpAsyncExecutionService -import com.itsaky.androidide.lsp.kotlin.compiler.services.DeclarationProviderFactory -import com.itsaky.androidide.lsp.kotlin.compiler.services.DeclarationProviderMerger -import com.itsaky.androidide.lsp.kotlin.compiler.services.ModificationTrackerFactory -import com.itsaky.androidide.lsp.kotlin.compiler.services.ModuleDependentsProvider -import com.itsaky.androidide.lsp.kotlin.compiler.services.PackagePartProviderFactory -import com.itsaky.androidide.lsp.kotlin.compiler.services.PackageProviderFactory -import com.itsaky.androidide.lsp.kotlin.compiler.services.PackageProviderMerger -import com.itsaky.androidide.lsp.kotlin.compiler.services.PlatformSettings -import com.itsaky.androidide.lsp.kotlin.compiler.services.ProjectStructureProvider -import org.jetbrains.kotlin.analysis.api.KaExperimentalApi -import org.jetbrains.kotlin.analysis.api.KaImplementationDetail -import org.jetbrains.kotlin.analysis.api.platform.KotlinPlatformSettings -import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory -import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory -import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderMerger -import org.jetbrains.kotlin.analysis.api.platform.lifetime.KotlinLifetimeTokenFactory -import org.jetbrains.kotlin.analysis.api.platform.lifetime.KotlinReadActionConfinementLifetimeTokenFactory -import org.jetbrains.kotlin.analysis.api.platform.modification.KotlinModificationTrackerFactory -import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackagePartProviderFactory -import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderFactory -import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderMerger -import org.jetbrains.kotlin.analysis.api.platform.permissions.KotlinAnalysisPermissionOptions -import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinModuleDependentsProvider -import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinProjectStructureProvider -import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.AnalysisApiSimpleServiceRegistrar -import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.PluginStructureProvider -import org.jetbrains.kotlin.analysis.decompiler.stub.file.ClsKotlinBinaryClassCache -import org.jetbrains.kotlin.analysis.decompiler.stub.file.DummyFileAttributeService -import org.jetbrains.kotlin.analysis.decompiler.stub.file.FileAttributeService -import org.jetbrains.kotlin.asJava.finder.JavaElementFinder -import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.mock.MockApplication -import org.jetbrains.kotlin.com.intellij.mock.MockProject -import org.jetbrains.kotlin.com.intellij.openapi.Disposable -import org.jetbrains.kotlin.com.intellij.openapi.application.AsyncExecutionService -import org.jetbrains.kotlin.com.intellij.psi.PsiElementFinder -import org.jetbrains.kotlin.com.intellij.psi.PsiFile -import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeEvent -import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeListener -import org.jetbrains.kotlin.com.intellij.psi.SmartPointerManager -import org.jetbrains.kotlin.com.intellij.psi.SmartTypePointerManager -import org.jetbrains.kotlin.com.intellij.psi.impl.PsiElementFinderImpl -import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl -import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartTypePointerManagerImpl -import org.jetbrains.kotlin.com.intellij.psi.impl.source.codeStyle.IndentHelper - -@OptIn(KaImplementationDetail::class) -internal object LspServiceRegistrar : AnalysisApiSimpleServiceRegistrar() { - - private const val PLUGIN_RELATIVE_PATH = "/META-INF/kt-lsp/kt-lsp.xml" - - override fun registerApplicationServices(application: MockApplication) { - PluginStructureProvider.registerApplicationServices(application, PLUGIN_RELATIVE_PATH) - - with(application) { - registerService(FileAttributeService::class.java, DummyFileAttributeService::class.java) - registerService( - KotlinAnalysisPermissionOptions::class.java, - AnalysisPermissionOptions::class.java - ) - registerService(ClsKotlinBinaryClassCache::class.java) - registerService(AsyncExecutionService::class.java, NoOpAsyncExecutionService::class.java) - } - } - - override fun registerProjectServices(project: MockProject) { - PluginStructureProvider.registerProjectServices(project, PLUGIN_RELATIVE_PATH) - - - with(project) { - registerService( - KotlinLifetimeTokenFactory::class.java, - KotlinReadActionConfinementLifetimeTokenFactory::class.java - ) - registerService(KotlinPlatformSettings::class.java, PlatformSettings::class.java) - registerService( - SmartTypePointerManager::class.java, - SmartTypePointerManagerImpl::class.java - ) - registerService(SmartPointerManager::class.java, SmartPointerManagerImpl::class.java) - registerService( - KotlinProjectStructureProvider::class.java, - ProjectStructureProvider::class.java - ) - registerService( - KotlinModuleDependentsProvider::class.java, - ModuleDependentsProvider::class.java - ) - registerService( - KotlinModificationTrackerFactory::class.java, - ModificationTrackerFactory::class.java - ) - registerService( - KotlinAnnotationsResolverFactory::class.java, - AnnotationsResolverFactory::class.java - ) - registerService( - KotlinDeclarationProviderFactory::class.java, - DeclarationProviderFactory::class.java - ) - registerService( - KotlinDeclarationProviderMerger::class.java, - DeclarationProviderMerger::class.java - ) - registerService( - KotlinPackageProviderFactory::class.java, - PackageProviderFactory::class.java - ) - registerService(KotlinPackageProviderMerger::class.java, PackageProviderMerger::class.java) - registerService( - KotlinPackagePartProviderFactory::class.java, - PackagePartProviderFactory::class.java - ) - } - } - - @OptIn(KaExperimentalApi::class) - @Suppress("TestOnlyProblems") - override fun registerProjectModelServices(project: MockProject, disposable: Disposable) { - with(PsiElementFinder.EP.getPoint(project)) { - registerExtension(JavaElementFinder(project), disposable) - registerExtension(PsiElementFinderImpl(project), disposable) - } - } -} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt index 56ff769c31..6dbe0e491e 100644 --- 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 @@ -2,7 +2,7 @@ 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 +class AnalysisPermissionOptions( + override val defaultIsAnalysisAllowedOnEdt: Boolean = false, + override val defaultIsAnalysisAllowedInWriteAction: Boolean = true, +) : KotlinAnalysisPermissionOptions \ No newline at end of file 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 index f7c0e271e6..b8e3498a21 100644 --- 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 @@ -68,17 +68,10 @@ class DeclarationProviderMerger(private val project: Project) : KotlinDeclaratio } } - -internal class DeclarationProvider( - val scope: GlobalSearchScope, - private val project: Project, - private val index: KtSymbolIndex +internal abstract class AbstractDeclarationProvider( + protected val project: Project, ) : KotlinDeclarationProvider { - - override val hasSpecificCallablePackageNamesComputation: Boolean - get() = false - override val hasSpecificClassifierPackageNamesComputation: Boolean - get() = false + protected abstract fun ktFilesForPackage(fqName: FqName): Sequence override fun findFilesForFacade(facadeFqName: FqName): Collection { if (facadeFqName.shortNameOrSpecial().isSpecial) return emptyList() @@ -88,8 +81,8 @@ internal class DeclarationProvider( } 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, + // 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() @@ -175,8 +168,18 @@ internal class DeclarationProvider( .filter { it.isTopLevel } .filter { it.nameAsName == callableId.callableName } .toList() +} + +internal class DeclarationProvider( + val scope: GlobalSearchScope, + project: Project, + private val index: KtSymbolIndex +) : AbstractDeclarationProvider(project) { + + override val hasSpecificCallablePackageNamesComputation = false + override val hasSpecificClassifierPackageNamesComputation = false - private fun ktFilesForPackage(fqName: FqName): Sequence { + override fun ktFilesForPackage(fqName: FqName): Sequence { return index.filesForPackage(fqName.asString()) .mapNotNull { VirtualFileManager.getInstance().findFileByNioPath(Paths.get(it.filePath)) } .filter { it in scope } 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 2092a26035..e9b5704529 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 @@ -17,7 +17,6 @@ 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 @@ -25,8 +24,6 @@ import kotlin.io.path.pathString internal class ProjectStructureProvider : KtLspService, KotlinProjectStructureProviderBase() { companion object { - private val logger = LoggerFactory.getLogger(ProjectStructureProvider::class.java) - fun getInstance(project: Project): ProjectStructureProvider { return KotlinProjectStructureProvider.getInstance(project) as ProjectStructureProvider } @@ -172,4 +169,4 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr return null } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexerTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexerTest.kt new file mode 100644 index 0000000000..bc4c20346d --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexerTest.kt @@ -0,0 +1,86 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.kotlin.fixtures.KtLspTest +import com.itsaky.androidide.progress.ICancelChecker +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.appdevforall.codeonthego.indexing.InMemoryIndex +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.util.BackgroundIndexer +import org.junit.Test + +class SourceFileIndexerTest : KtLspTest() { + + private fun buildSymbolIndex(): JvmSymbolIndex { + val backing = InMemoryIndex(JvmSymbolDescriptor) + return object : JvmSymbolIndex(backing, BackgroundIndexer(backing)) { + override fun isActive(sourceId: String): Boolean = true + } + } + + @Test + fun `indexer ignores local property inside function body`(): Unit = runBlocking { + val file = createSourceFile( + "PollingWorker.kt", """ + fun doWork() { + for (orderId in listOf("1", "2")) { + try { + val response = fetchStatus(orderId) + if (response != null) { + val otpCode = response.otpCode ?: response.data?.otpCode + val status = response.data?.status ?: response.localStatus + if (otpCode != null) println(otpCode) + } + } catch (_: Exception) {} + } + } + fun fetchStatus(id: String): Any? = null + """.trimIndent() + ) + + val symbolsIndex = buildSymbolIndex() + + indexSourceFile(env.project, file, mockk(relaxed = true), symbolsIndex, ICancelChecker.NOOP) + + val names = symbolsIndex.findByPrefix("").map { it.shortName }.toSet() + assertThat(names).containsExactly("doWork", "fetchStatus") + } + + @Test + fun `indexer ignores local function inside function body`(): Unit = runBlocking { + val file = createSourceFile( + "Helpers.kt", """ + fun outer(): Int { + fun inner() = 42 + return inner() + } + """.trimIndent() + ) + + val symbolsIndex = buildSymbolIndex() + + indexSourceFile(env.project, file, mockk(relaxed = true), symbolsIndex, ICancelChecker.NOOP) + + val names = symbolsIndex.findByPrefix("").map { it.shortName }.toSet() + assertThat(names).containsExactly("outer") + } + + @Test + fun `top-level property and function are both indexed`(): Unit = runBlocking { + val file = createSourceFile( + "TopLevel.kt", """ + val globalConfig: String = "default" + fun compute(): Int = 0 + """.trimIndent() + ) + + val symbolsIndex = buildSymbolIndex() + + indexSourceFile(env.project, file, mockk(relaxed = true), symbolsIndex, ICancelChecker.NOOP) + + val names = symbolsIndex.findByPrefix("").map { it.shortName }.toSet() + assertThat(names).containsExactly("globalConfig", "compute") + } +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/ContentScopeStalenessTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/ContentScopeStalenessTest.kt new file mode 100644 index 0000000000..0e6b99f7f2 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/ContentScopeStalenessTest.kt @@ -0,0 +1,48 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.modules + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.kotlin.fixtures.KtLspTest +import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.junit.Test + +class ContentScopeStalenessTest : KtLspTest() { + + @Test + fun `file resolvable by path but missing from a stale scope snapshot can still be analyzed`() { + val a = createSourceFile( + "demo/A.kt", """ + package demo + fun a(): Int = 1 + """.trimIndent() + ) + + // Materialize the module's content scope and resolution scope NOW, while + // A.kt is the only file on disk. Previously, this used to freeze + // a module's filesScope that contains only A.kt. + analyze(a) { + val fn = a.declarations.filterIsInstance().first() + assertThat(fn.symbol).isNotNull() + } + + // B.kt is written AFTER the snapshot, WITHOUT invalidating the module scope. + // This simulates a file appearing (or being refreshed to a new VirtualFile + // instance) after the scope was last computed. + val b = env.createSourceFile( + "demo/B.kt", """ + package demo + fun b(): Int = 2 + """.trimIndent(), + ) + + // B.kt is resolvable to the same source module by path, so analysis opens a + // session for that module, but the stale snapshot scope does not contain B.kt. + // This used to throw a KaBaseIllegalPsiException. + analyze(b) { + val fn = b.declarations.filterIsInstance().first() + val sym = fn.symbol as? KaNamedFunctionSymbol + assertThat(sym).isNotNull() + assertThat(sym!!.name.asString()).isEqualTo("b") + } + } +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTest.kt new file mode 100644 index 0000000000..63063d3cf6 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTest.kt @@ -0,0 +1,30 @@ +package com.itsaky.androidide.lsp.kotlin.fixtures + +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.psi.KtFile +import org.junit.Rule +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +/** + * Base class for plain JVM unit tests that need a real Kotlin Analysis API environment. + * + * Each test method gets a fresh [KtLspTestEnvironment] (see [KtLspTestRule]) backed by a + * temporary source directory. + */ +@RunWith(RobolectricTestRunner::class) +abstract class KtLspTest { + + @get:Rule + @PublishedApi + internal val lspTestRule = KtLspTestRule() + + internal val env: KtLspTestEnvironment + get() = lspTestRule.env + + protected fun createSourceFile(relativePath: String, content: String): KtFile = + env.createSourceFile(relativePath, content) + + protected fun analyze(file: KtFile, action: KaSession.() -> R): R = + env.analyze(file, action) +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestEnvironment.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestEnvironment.kt new file mode 100644 index 0000000000..54e2337745 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestEnvironment.kt @@ -0,0 +1,242 @@ +package com.itsaky.androidide.lsp.kotlin.fixtures + +import com.itsaky.androidide.lsp.kotlin.compiler.AbstractCompilationEnvironment +import com.itsaky.androidide.lsp.kotlin.compiler.CompilationKind +import com.itsaky.androidide.lsp.kotlin.compiler.DEFAULT_LANGUAGE_VERSION +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.buildKtLibraryModule +import com.itsaky.androidide.lsp.kotlin.compiler.read +import com.itsaky.androidide.lsp.kotlin.compiler.registrar.AnalysisApiServiceProviders +import com.itsaky.androidide.lsp.kotlin.compiler.registrar.LspAnalysisApiServiceRegistrar +import com.itsaky.androidide.lsp.kotlin.compiler.services.AnalysisPermissionOptions +import org.appdevforall.codeonthego.indexing.InMemoryIndex +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataDescriptor +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex +import org.appdevforall.codeonthego.indexing.util.BackgroundIndexer +import org.jetbrains.kotlin.K1Deprecation +import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.platform.permissions.KotlinAnalysisPermissionOptions +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.AnalysisApiSimpleServiceRegistrar +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.StandaloneProjectFactory +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironmentMode +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.vfs.local.CoreLocalFileSystem +import org.jetbrains.kotlin.com.intellij.psi.PsiManager +import org.jetbrains.kotlin.config.LanguageVersion +import org.jetbrains.kotlin.psi.KtFile +import java.nio.file.Path +import kotlin.io.path.name +import kotlin.io.path.pathString +import kotlin.io.path.writeText +import org.jetbrains.kotlin.analysis.api.analyze as ktAnalyze + +/** + * A self-contained Kotlin Analysis API environment for use in plain JVM unit tests. + * + * @param sourceRoots Directories containing Kotlin/Java source files for the test. + * @param extraLibraryJars Additional JARs to add as library modules. + * @param languageVersion Kotlin language version; defaults to [DEFAULT_LANGUAGE_VERSION]. + * @param jdkRelease JDK release version; defaults to the host JVM's feature version. + */ +@OptIn(K1Deprecation::class, KaImplementationDetail::class) +internal class KtLspTestEnvironment( + val sourceRoots: List, + private val extraLibraryJars: List = emptyList(), + languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, + jdkRelease: Int = checkNotNull(System.getProperty("java.specification.version")).toInt(), +) : AbstractCompilationEnvironment( + name = "test", + kind = CompilationKind.Default, + intellijPluginRoot = findIntellijPluginRoot(), + jdkHome = Path.of(System.getProperty("java.home")), + jdkRelease = jdkRelease, + languageVersion = languageVersion, + applicationEnvironmentMode = KotlinCoreApplicationEnvironmentMode.UnitTest, + enableParserEventSystem = false, +) { + private lateinit var localFileSystem: CoreLocalFileSystem + + init { + initialize(::buildModules, ::buildKtSymbolIndex) + } + + override fun createServiceRegistrars(): List { + return listOf( + LspAnalysisApiServiceRegistrar( + provider = AnalysisApiServiceProviders.Production + .toBuilder() + .apply { + appService(KotlinAnalysisPermissionOptions::class, replace = true) { + AnalysisPermissionOptions(defaultIsAnalysisAllowedOnEdt = true) + } + } + .build() + ) + ) + } + + override fun postInit(libraryRoots: List) { + super.postInit(libraryRoots) + localFileSystem = applicationEnv.localFileSystem + } + + private fun buildModules( + project: MockProject, + applicationEnv: KotlinCoreApplicationEnvironment, + ): List { + val jdkModule = buildKtLibraryModule(project, applicationEnv) { + id = "jdk" + isSdk = true + addContentRoot(jdkHome) + } + + val stdlibModule = findKotlinStdlibJar()?.let { jar -> + buildKtLibraryModule(project, applicationEnv) { + id = jar.pathString + addContentRoot(jar) + addDependency(jdkModule) + } + } + + val extraLibModules = extraLibraryJars.map { jar -> + buildKtLibraryModule(project, applicationEnv) { + id = jar.pathString + addContentRoot(jar) + addDependency(jdkModule) + stdlibModule?.let { addDependency(it) } + } + } + + val sourceDeps: List = buildList { + add(jdkModule) + stdlibModule?.let { add(it) } + addAll(extraLibModules) + } + + val sourceModules = sourceRoots.mapIndexed { i, root -> + TestKtSourceModule( + project = project, + name = "test-source-$i", + roots = setOf(root), + dependencies = sourceDeps, + languageVersion = languageVersion, + ) + } + + return buildList { + addAll(sourceModules) + stdlibModule?.let { add(it) } + addAll(extraLibModules) + add(jdkModule) + } + } + + private fun buildKtSymbolIndex( + modules: List, + libraryRoots: List, + ): KtSymbolIndex { + val inMemoryJvmBackingIndex = InMemoryIndex(JvmSymbolDescriptor) + val inMemoryJvmSymbolIndex = object : JvmSymbolIndex(inMemoryJvmBackingIndex, BackgroundIndexer(inMemoryJvmBackingIndex)) { + // ensure we're not filtering out anything + override fun isActive(sourceId: String) = true + } + + val inMemoryFileMetaBackingIndex = InMemoryIndex(KtFileMetadataDescriptor) + val inMemoryFileMetaIndex = KtFileMetadataIndex(inMemoryFileMetaBackingIndex) + + return KtSymbolIndex( + kind = kind, + project = project, + modules = modules, + fileIndex = inMemoryFileMetaIndex, + sourceIndex = inMemoryJvmSymbolIndex, + libraryIndex = inMemoryJvmSymbolIndex, + ) + } + + /** + * Writes [content] to [relativePath] under the first source root, refreshes + * the VFS, and returns the corresponding [KtFile]. + */ + fun createSourceFile( + relativePath: String, + content: String, + ): KtFile { + require(sourceRoots.isNotEmpty()) { "No source roots configured" } + val file = sourceRoots.first().resolve(relativePath) + file.parent.toFile().mkdirs() + file.writeText(content) + + val vf = localFileSystem.refreshAndFindFileByPath(file.pathString) + ?: error("VFS cannot find newly created file: $file") + + modules.filterIsInstance().forEach { it.invalidateSearchScope() } + + return project.read { + PsiManager.getInstance(project).findFile(vf) as? KtFile + ?: error("PSI file not found for: $file") + } + } + + /** + * Runs [action] inside a [KaSession] for [file], acquiring the project read lock first. + */ + inline fun analyze(file: KtFile, crossinline action: KaSession.() -> R): R = + project.read { ktAnalyze(file, action) } +} + +/** + * Locates the kotlin-android embeddable JAR that serves as the IntelliJ plugin + * root for the Analysis API. + * + * The JAR is cached by the `externalAssets` Gradle plugin under the name + * `kt-android.jar` (derived from `jarDependency("kt-android")`). We find it + * on the test classpath by name, which works reliably under both plain-JVM and + * Robolectric classloaders. A reflection-based fallback handles any environment + * where the JAR name differs. + */ +private fun findIntellijPluginRoot(): Path { + // Primary: scan the classpath for the well-known cached names. + val classPath = System.getProperty("java.class.path") ?: "" + classPath.split(java.io.File.pathSeparator) + .map { Path.of(it) } + .firstOrNull { + // named 'kt-android.jar' when added by external assets plugins + it.name == "kt-android.jar" || + + // for local builds, named 'analysis-api-standalone-embeddable-for-ide-X.X.X-SNAPSHOT.jar' + it.name.matches("analysis-api-standalone-embeddable-for-ide.*\\.jar".toRegex()) + } + ?.let { return it } + + // Fallback to reflection. This works on a plain JVM where the classloader exposes + // the code source, but may not work under Robolectric. + return try { + val location = StandaloneProjectFactory::class.java.protectionDomain + ?.codeSource?.location + ?: error("code source is null") + val path = Path.of(location.toURI()) + check(path.name.endsWith(".jar")) { "resolved to directory, not a JAR: $path" } + path + } catch (e: Exception) { + error( + "Cannot locate kt-android.jar on the test classpath. " + + "Ensure the subprojects.kotlinAnalysisApi dependency is included in testImplementation. " + + "Also verify that the JAR file name matches expected names. " + + "(reflection fallback also failed: ${e.message})" + ) + } +} + +private fun findKotlinStdlibJar(): Path? = try { + val location = KotlinVersion::class.java.protectionDomain?.codeSource?.location ?: return null + Path.of(location.toURI()).takeIf { it.name.endsWith(".jar") } +} catch (_: Exception) { + null +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestEnvironmentTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestEnvironmentTest.kt new file mode 100644 index 0000000000..058162fe20 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestEnvironmentTest.kt @@ -0,0 +1,50 @@ +package com.itsaky.androidide.lsp.kotlin.fixtures + +import com.google.common.truth.Truth.assertThat +import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.junit.Test + +class KtLspTestEnvironmentTest : KtLspTest() { + + @Test + fun `source file is created and visible to the project`() { + val file = createSourceFile("Hello.kt", "fun hello() = \"hi\"") + assertThat(file.name).isEqualTo("Hello.kt") + assertThat(file.declarations).hasSize(1) + } + + @Test + fun `analysis resolves named function symbol`() { + val file = createSourceFile("Greet.kt", """ + fun greet(name: String): String = "Hello, ${'$'}name" + """.trimIndent()) + + analyze(file) { + val fn = file.declarations.filterIsInstance().first() + val sym = fn.symbol as? KaNamedFunctionSymbol + assertThat(sym).isNotNull() + assertThat(sym!!.name.asString()).isEqualTo("greet") + } + } + + @Test + fun `analysis resolves cross-file class reference`() { + createSourceFile("com/example/Foo.kt", """ + package com.example + class Foo(val value: Int) + """.trimIndent()) + + val file = createSourceFile("com/example/Bar.kt", """ + package com.example + fun makeFoo(): Foo = Foo(42) + """.trimIndent()) + + analyze(file) { + val fn = file.declarations.filterIsInstance().first() + val sym = fn.symbol as? KaNamedFunctionSymbol + assertThat(sym).isNotNull() + assertThat(sym!!.returnType.toString()).contains("Foo") + } + } +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestRule.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestRule.kt new file mode 100644 index 0000000000..e80b8c0044 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/KtLspTestRule.kt @@ -0,0 +1,39 @@ +package com.itsaky.androidide.lsp.kotlin.fixtures + +import com.itsaky.androidide.lsp.kotlin.compiler.write +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +internal class KtLspTestRule : TestRule { + + val tempDir = TemporaryFolder() + lateinit var env: KtLspTestEnvironment + private set + + override fun apply( + statement: Statement?, + p1: Description? + ): Statement { + return object : Statement() { + override fun evaluate() { + try { + tempDir.create() + + val sourceRoot = tempDir.newFolder("src").toPath() + env = KtLspTestEnvironment(listOf(sourceRoot)) + + statement?.evaluate() + } finally { + if (::env.isInitialized) { + env.project.write { + // TODO: This fails in test cases, ignored for now + // env.close() + } + } + } + } + } + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/TestKtSourceModule.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/TestKtSourceModule.kt new file mode 100644 index 0000000000..ceddcf33a3 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/fixtures/TestKtSourceModule.kt @@ -0,0 +1,42 @@ +package com.itsaky.androidide.lsp.kotlin.fixtures + +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.modules.AbstractSourceModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +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.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 java.nio.file.Path + +@OptIn(KaPlatformInterface::class, KaExperimentalApi::class) +internal class TestKtSourceModule( + project: Project, + override val name: String, + roots: Set, + dependencies: List = emptyList(), + languageVersion: LanguageVersion = DEFAULT_LANGUAGE_VERSION, + jvmTarget: JvmTarget = DEFAULT_JVM_TARGET, +) : KaSourceModule, AbstractSourceModule(project, dependencies) { + + override val id: String = name + override val contentRoots: Set = roots + override val moduleDescription: String = "test:$name" + + override val languageVersionSettings: LanguageVersionSettings = + LanguageVersionSettingsImpl( + languageVersion = languageVersion, + apiVersion = ApiVersion.createByLanguageVersion(languageVersion), + ) + + override val targetPlatform: TargetPlatform = + JvmPlatforms.jvmPlatformByTargetVersion(jvmTarget) +} diff --git a/app/src/main/java/com/itsaky/androidide/utils/Either.kt b/shared/src/main/java/com/itsaky/androidide/utils/Either.kt similarity index 99% rename from app/src/main/java/com/itsaky/androidide/utils/Either.kt rename to shared/src/main/java/com/itsaky/androidide/utils/Either.kt index 4e9247377b..84480b2795 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/Either.kt +++ b/shared/src/main/java/com/itsaky/androidide/utils/Either.kt @@ -27,4 +27,4 @@ data class Either( fun right(value: R) = Either(_right = value) } -} +} \ No newline at end of file diff --git a/subprojects/kotlin-analysis-api/build.gradle.kts b/subprojects/kotlin-analysis-api/build.gradle.kts index 50c3ae79af..34e3e05a07 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}-172a7e7" +val ktAndroidTag = "v${ktAndroidVersion}-06e90ae" 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 = "ee52466a893ed7261fb542a259cb469227aa8059cf7a36b5d1b41897a7e5bb08", + sha256Checksum = "2069ed685dafd6eed36ebe242004ed5e24e28360293117323e2c988afefa6767", ) } } diff --git a/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt b/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt index cdc5e85331..a7bbf7790e 100644 --- a/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt +++ b/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt @@ -101,7 +101,7 @@ abstract class LSPTest { // We need to manually setup the language server with the project here // ProjectManager.notifyProjectUpdate() ILanguageServerRegistry - .getDefault() + .default .getServer(getServerId())!! .setupWithProject(projectManager.workspace!!)