Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for IntelliJ 2023.1 #4037

Merged
merged 8 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)

Expand Down
1 change: 1 addition & 0 deletions sqldelight-idea-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -66,7 +65,7 @@ private class SqlDelightIdentifierHandler(
): Boolean {
val generatedFiles = element.generatedVirtualFiles()
val ignoringFileProcessor = Processor<UsageInfo> { t ->
if (t is KotlinReferenceUsageInfo && t.virtualFile in generatedFiles) {
if (factory.isKotlinReferenceUsageInfo(t) && t.virtualFile in generatedFiles) {
return@Processor true
}
processor.process(t)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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<Path>
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<Path>
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<Path>).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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}