diff --git a/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/SqlDelightEnvironment.kt b/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/SqlDelightEnvironment.kt index d550e5fd6fe..7ffe3627936 100644 --- a/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/SqlDelightEnvironment.kt +++ b/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/SqlDelightEnvironment.kt @@ -37,6 +37,7 @@ import com.alecstrong.sql.psi.core.psi.SqlCreateTableStmt import com.alecstrong.sql.psi.core.psi.SqlStmt import com.intellij.core.CoreApplicationEnvironment import com.intellij.mock.MockModule +import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.module.Module import com.intellij.openapi.roots.ModuleExtension import com.intellij.openapi.vfs.VirtualFile @@ -80,9 +81,10 @@ class SqlDelightEnvironment( init { project.registerService(SqlDelightProjectService::class.java, this) + @Suppress("UnresolvedPluginConfigReference") CoreApplicationEnvironment.registerExtensionPoint( module.extensionArea, - ModuleExtension.EP_NAME, + ExtensionPointName.create("com.intellij.moduleExtension"), ModuleExtension::class.java, ) diff --git a/sqldelight-idea-plugin/build.gradle b/sqldelight-idea-plugin/build.gradle index fc2021e0b70..9140f17dff7 100644 --- a/sqldelight-idea-plugin/build.gradle +++ b/sqldelight-idea-plugin/build.gradle @@ -42,6 +42,7 @@ tasks.named('runPluginVerifier') { "IC-2022.1.4", // AS: Electric Eel (2022.1.1) RC 1 "IC-2022.2.4", // IC "IC-2022.3.1", // IC + "IC-2023.1", // IC ] def customFailureLevel = FailureLevel.ALL diff --git a/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/SqlDelightFindUsagesHandlerFactory.kt b/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/SqlDelightFindUsagesHandlerFactory.kt index 96eec44a332..592272f26ed 100644 --- a/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/SqlDelightFindUsagesHandlerFactory.kt +++ b/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/SqlDelightFindUsagesHandlerFactory.kt @@ -5,6 +5,7 @@ import app.cash.sqldelight.core.lang.SqlDelightFile import app.cash.sqldelight.core.lang.psi.StmtIdentifierMixin import app.cash.sqldelight.core.lang.queriesName import app.cash.sqldelight.core.psi.SqlDelightStmtIdentifier +import app.cash.sqldelight.intellij.usages.ReflectiveKotlinFindUsagesFactory import com.alecstrong.sql.psi.core.psi.SqlColumnName import com.intellij.find.findUsages.FindUsagesHandler import com.intellij.find.findUsages.FindUsagesHandlerFactory @@ -20,8 +21,6 @@ import com.intellij.psi.PsiManager import com.intellij.psi.util.PsiTreeUtil import com.intellij.usageView.UsageInfo import com.intellij.util.Processor -import org.jetbrains.kotlin.idea.findUsages.KotlinFindUsagesHandlerFactory -import org.jetbrains.kotlin.idea.findUsages.KotlinReferenceUsageInfo import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtNamedDeclaration @@ -43,7 +42,7 @@ class SqlDelightFindUsagesHandlerFactory : FindUsagesHandlerFactory() { private class SqlDelightIdentifierHandler( private val element: PsiElement, ) : FindUsagesHandler(element) { - private val factory = KotlinFindUsagesHandlerFactory(element.project) + private val factory = ReflectiveKotlinFindUsagesFactory(element.project) private val kotlinHandlers = when (element) { is StmtIdentifierMixin -> element.generatedMethods() is SqlColumnName -> element.generatedProperties() @@ -66,7 +65,7 @@ private class SqlDelightIdentifierHandler( ): Boolean { val generatedFiles = element.generatedVirtualFiles() val ignoringFileProcessor = Processor { t -> - if (t is KotlinReferenceUsageInfo && t.virtualFile in generatedFiles) { + if (factory.isKotlinReferenceUsageInfo(t) && t.virtualFile in generatedFiles) { return@Processor true } processor.process(t) diff --git a/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/gradle/FileIndexMap.kt b/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/gradle/FileIndexMap.kt index 8cdc6cea83e..cf3a7eee3b0 100644 --- a/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/gradle/FileIndexMap.kt +++ b/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/gradle/FileIndexMap.kt @@ -22,6 +22,7 @@ import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.ui.EditorNotifications +import com.intellij.util.lang.ClassPath import com.intellij.util.lang.UrlClassLoader import org.jetbrains.plugins.gradle.service.execution.GradleExecutionHelper import org.jetbrains.plugins.gradle.settings.DistributionType @@ -135,6 +136,8 @@ internal class FileIndexMap { EditorNotifications.getInstance(module.project).updateAllNotifications() } catch (externalException: ExternalSystemException) { // It's a gradle error, ignore and let the user fix when they try and build the project + Timber.i("sqldelight model gen failed") + Timber.i(externalException) FileIndexingNotification.getInstance(project).unconfiguredReason = FileIndexingNotification.UnconfiguredReason.Incompatible( @@ -161,22 +164,50 @@ internal class FileIndexMap { val pluginClassLoader = pluginClassLoader as UrlClassLoader // We need to remove the last loaded dialect as well as add our new one. - val files = UrlClassLoader::class.java.getDeclaredField("files").let { field -> - field.isAccessible = true - val result = field.get(pluginClassLoader) as MutableList - field.isAccessible = false - return@let result + val files = try { + UrlClassLoader::class.java.getDeclaredField("files").let { field -> + field.isAccessible = true + val result = field.get(pluginClassLoader) as List + field.isAccessible = false + return@let result + } + } catch (e: NoSuchFieldException) { + // This is a newer version of IntelliJ that doesn't have the files field on UrlClassLoader, + // reflect on Classpath instead. + ClassPath::class.java.getDeclaredField("files").let { field -> + field.isAccessible = true + val result = (field.get(pluginClassLoader.classPath) as Array).toList() + field.isAccessible = false + return@let result + } } - // Remove the last loaded dialect. - previouslyAddedDialect?.let { - files.removeAll(it) - } + // Filter out the last loaded dialect. + val filtered = files.filter { it != previouslyAddedDialect } + val newClasspath = filtered + dialectPath previouslyAddedDialect = dialectPath // Add the new one in. - files.addAll(dialectPath) - pluginClassLoader.classPath.reset(files) + try { + // older IntelliJ versions have a reset method that takes a list of files. + ClassPath::class.java.getDeclaredMethod("reset", List::class.java).let { method -> + method.isAccessible = true + method.invoke(pluginClassLoader.classPath, newClasspath) + method.isAccessible = false + } + } catch (e: NoSuchMethodException) { + // in newer versions of IntelliJ, call both argless reset and set files reflectively. + ClassPath::class.java.getDeclaredMethod("reset").let { method -> + method.isAccessible = true + method.invoke(pluginClassLoader.classPath) + method.isAccessible = false + } + ClassPath::class.java.getDeclaredField("files").let { field -> + field.isAccessible = true + field.set(pluginClassLoader.classPath, newClasspath.toTypedArray()) + field.isAccessible = false + } + } return shouldInvalidate } diff --git a/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/usages/ReflectiveKotlinFindUsagesFactory.kt b/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/usages/ReflectiveKotlinFindUsagesFactory.kt new file mode 100644 index 00000000000..de8c2f39be6 --- /dev/null +++ b/sqldelight-idea-plugin/src/main/kotlin/app/cash/sqldelight/intellij/usages/ReflectiveKotlinFindUsagesFactory.kt @@ -0,0 +1,56 @@ +package app.cash.sqldelight.intellij.usages + +import com.intellij.find.findUsages.FindUsagesHandler +import com.intellij.find.findUsages.FindUsagesHandlerFactory +import com.intellij.find.findUsages.FindUsagesOptions +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.usageView.UsageInfo +import java.lang.reflect.Method + +class ReflectiveKotlinFindUsagesFactory private constructor( + private val wrapped: FindUsagesHandlerFactory, +) { + constructor(project: Project) : this(createKotlinFindUsagesHandlerFactory(project)) + + private val findFunctionOptionsMethod: Method = wrapped.javaClass.getMethod("getFindFunctionOptions") + private val findPropertyOptionsMethod: Method = wrapped.javaClass.getMethod("getFindPropertyOptions") + + val findFunctionOptions get() = findFunctionOptionsMethod.invoke(wrapped) as FindUsagesOptions + val findPropertyOptions get() = findPropertyOptionsMethod.invoke(wrapped) as FindUsagesOptions + + fun createFindUsagesHandler(element: PsiElement, forHighlightUsages: Boolean): FindUsagesHandler { + return wrapped.createFindUsagesHandler(element, forHighlightUsages)!! + } + + fun isKotlinReferenceUsageInfo(info: UsageInfo): Boolean { + return kotlinReferenceUsageInfoClass.isAssignableFrom(info.javaClass) + } + + companion object { + // IC 2023.1 or later + private const val kotlinUsagePackage = "org.jetbrains.kotlin.idea.base.searching.usages" + + // older than IC 2023.1 + private const val legacyKotlinUsagePackage = "org.jetbrains.kotlin.idea.findUsages" + + private val factoryClass = try { + Class.forName("$kotlinUsagePackage.KotlinFindUsagesHandlerFactory") + } catch (e: ClassNotFoundException) { + // fall back to older version + Class.forName("$legacyKotlinUsagePackage.KotlinFindUsagesHandlerFactory") + } + + private val kotlinReferenceUsageInfoClass = try { + Class.forName("$kotlinUsagePackage.KotlinReferenceUsageInfo") + } catch (e: ClassNotFoundException) { + // fall back to older version + Class.forName("$legacyKotlinUsagePackage.KotlinReferenceUsageInfo") + } + + private fun createKotlinFindUsagesHandlerFactory(project: Project): FindUsagesHandlerFactory { + val ctor = factoryClass.getConstructor(Project::class.java) + return ctor.newInstance(project) as FindUsagesHandlerFactory + } + } +}