<NME> AddQualifierFix.kt
<BEF> package org.elm.ide.inspections.import

import com.intellij.codeInspection.LocalQuickFixOnPsiElement
import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import org.elm.lang.core.psi.*
import org.elm.lang.core.psi.elements.*
import org.elm.lang.core.psi.elements.Flavor.*
import org.elm.lang.core.resolve.ElmReferenceElement
import org.elm.lang.core.resolve.scope.ModuleScope
import org.elm.lang.core.resolve.scope.VisibleNames
import org.elm.openapiext.isUnitTestMode
import org.elm.openapiext.runWriteCommandAction
import org.elm.openapiext.runWriteCommandAction
import org.jetbrains.annotations.TestOnly

class AddQualifierFix(element: ElmPsiElement) : LocalQuickFixOnPsiElement(element) {

    data class Context(
            val candidates: List<String>,
            val qid: ElmQID
            val qid: ElmQID
    )

    override fun getText() = "Qualify name"
    override fun getFamilyName() = text

    public override fun isAvailable(): Boolean {
        return super.isAvailable() && findApplicableContext() != null
    }

    private fun findApplicableContext(): Context? {
        val element = startElement as? ElmPsiElement ?: return null

        if (element.ancestors.any { it is ElmModuleDeclaration || it is ElmPortAnnotation || it is ElmImportClause }) {
            return null
        }

        val file = element.elmFile

        return when (element) {
            is ElmTypeRef -> makeContext(file, element.upperCaseQID, element, ModuleScope.getReferencableTypes(file))
            is ElmUnionPattern -> makeContext(file, element.upperCaseQID, element, ModuleScope.getReferencableConstructors(file))
            is ElmValueExpr -> when (element.flavor) {
                QualifiedValue, QualifiedConstructor -> null
                BareValue -> {
                    makeContext(file, element.qid, element, ModuleScope.getReferencableValues(file))
                }
                BareConstructor -> {
                    makeContext(file, element.qid, element, ModuleScope.getReferencableConstructors(file))
                }
            }
            else -> null
        }
    }

    private fun makeContext(file: ElmFile, qid: ElmQID, ref: ElmReferenceElement, names: VisibleNames): Context? {
        val name = ref.referenceName
        val candidates = (names.global + names.imported)
                .filter { it.name == name }
                .mapNotNull { ModuleScope.getQualifierForName(file, it.moduleName, name) }
                .filter { it != "" }
        if (candidates.isEmpty()) return null
        return Context(candidates, name, qid)
    }

    override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
        if (file !is ElmFile) return
        val context = findApplicableContext() ?: return

        when (context.candidates.size) {
            0 -> error("should not happen: must be at least one candidate")
    }

    private fun addQualifier(project: Project, context: Context, qualifier: String) {
        val factory = ElmPsiFactory(project)
        val newName = qualifier + context.referenceName
        val newId = when (context.qid) {
            is ElmUpperCaseQID -> factory.createUpperCaseQID(newName)
            is ElmValueQID -> factory.createValueQID(newName)
            else -> error("unexpected QID type")
        }
        context.qid.replace(newId)
    }

    private fun promptToSelectCandidate(project: Project, context: Context) {
        require(context.candidates.isNotEmpty())

        DataManager.getInstance().dataContextFromFocusAsync.onSuccess { dataContext ->
            val picker = if (isUnitTestMode) {
                MOCK ?: error("You must set mock UI via `withMockQualifierPickerUI`")
            } else {
                RealQualifierPickerUI(dataContext, context)
            }
            picker.choose(context.candidates) { qualifier ->
                project.runWriteCommandAction {
                    addQualifier(project, context, qualifier)
                }
            }
        }
    }
}

interface QualifierPickerUI {
    fun choose(qualifiers: List<String>, callback: (String) -> Unit)
}

private var MOCK: QualifierPickerUI? = null

@TestOnly
fun withMockQualifierPickerUI(mockUi: QualifierPickerUI, action: () -> Unit) {
    MOCK = mockUi
    try {
        action()
    } finally {
        MOCK = null
    }
}

private class RealQualifierPickerUI(val dataContext: DataContext, val context: AddQualifierFix.Context) : QualifierPickerUI {
    override fun choose(qualifiers: List<String>, callback: (String) -> Unit) {
        JBPopupFactory.getInstance().createPopupChooserBuilder(qualifiers)
                .setTitle("Add qualifier to '${context.referenceName}':")
                .setItemChosenCallback { callback(it) }
                .setNamerForFiltering { it }
                .createPopup().showInBestPositionFor(dataContext)
    }
}

<MSG> Fix memory leaks in quick fixes (#596)

According to Kotlin's [inspection authoring guidelines](https://github.com/JetBrains/kotlin/blob/master/docs/intentions_inspections_quickfixes.md):

> There shouldn't be PSI elements stored in QuickFix classes (val psi: PsiElement) as such elements might be invalidated and can lead to memory leaks. Smart pointer (check SmartPsiElementPointer class and createSmartPointer() function) can be used when such storage is absolutely necessary.

The `quickFix` convenience function I wrote encouraged callers to capture PsiElements in the lambda, so I removed it in favor of a `NamedQuickFix` base class and stopped storing PsiElements. I also removed usages of `LocalQuickFixOnPsiElement` when not necessary, and added docs so we don't forget this limitation.
<DFF> @@ -1,12 +1,12 @@
 package org.elm.ide.inspections.import
 
-import com.intellij.codeInspection.LocalQuickFixOnPsiElement
+import com.intellij.codeInspection.ProblemDescriptor
 import com.intellij.ide.DataManager
 import com.intellij.openapi.actionSystem.DataContext
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.ui.popup.JBPopupFactory
 import com.intellij.psi.PsiElement
-import com.intellij.psi.PsiFile
+import org.elm.ide.inspections.NamedQuickFix
 import org.elm.lang.core.psi.*
 import org.elm.lang.core.psi.elements.*
 import org.elm.lang.core.psi.elements.Flavor.*
@@ -18,7 +18,7 @@ import org.elm.openapiext.isUnitTestMode
 import org.elm.openapiext.runWriteCommandAction
 import org.jetbrains.annotations.TestOnly
 
-class AddQualifierFix(element: ElmPsiElement) : LocalQuickFixOnPsiElement(element) {
+class AddQualifierFix : NamedQuickFix("Qualify name") {
 
     data class Context(
             val candidates: List<String>,
@@ -26,51 +26,49 @@ class AddQualifierFix(element: ElmPsiElement) : LocalQuickFixOnPsiElement(elemen
             val qid: ElmQID
     )
 
-    override fun getText() = "Qualify name"
-    override fun getFamilyName() = text
-
-    public override fun isAvailable(): Boolean {
-        return super.isAvailable() && findApplicableContext() != null
-    }
-
-    private fun findApplicableContext(): Context? {
-        val element = startElement as? ElmPsiElement ?: return null
-
-        if (element.ancestors.any { it is ElmModuleDeclaration || it is ElmPortAnnotation || it is ElmImportClause }) {
-            return null
+    companion object {
+        fun isAvailable(psiElement: PsiElement?): Boolean {
+            return findApplicableContext(psiElement) != null
         }
 
-        val file = element.elmFile
+        private fun findApplicableContext(psiElement: PsiElement?): Context? {
+            val element = psiElement as? ElmPsiElement ?: return null
 
-        return when (element) {
-            is ElmTypeRef -> makeContext(file, element.upperCaseQID, element, ModuleScope.getReferencableTypes(file))
-            is ElmUnionPattern -> makeContext(file, element.upperCaseQID, element, ModuleScope.getReferencableConstructors(file))
-            is ElmValueExpr -> when (element.flavor) {
-                QualifiedValue, QualifiedConstructor -> null
-                BareValue -> {
-                    makeContext(file, element.qid, element, ModuleScope.getReferencableValues(file))
-                }
-                BareConstructor -> {
-                    makeContext(file, element.qid, element, ModuleScope.getReferencableConstructors(file))
+            if (element.ancestors.any { it is ElmModuleDeclaration || it is ElmPortAnnotation || it is ElmImportClause }) {
+                return null
+            }
+
+            val file = element.elmFile
+
+            return when (element) {
+                is ElmTypeRef -> makeContext(file, element.upperCaseQID, element, ModuleScope.getReferencableTypes(file))
+                is ElmUnionPattern -> makeContext(file, element.upperCaseQID, element, ModuleScope.getReferencableConstructors(file))
+                is ElmValueExpr -> when (element.flavor) {
+                    QualifiedValue, QualifiedConstructor -> null
+                    BareValue -> {
+                        makeContext(file, element.qid, element, ModuleScope.getReferencableValues(file))
+                    }
+                    BareConstructor -> {
+                        makeContext(file, element.qid, element, ModuleScope.getReferencableConstructors(file))
+                    }
                 }
+                else -> null
             }
-            else -> null
         }
-    }
 
-    private fun makeContext(file: ElmFile, qid: ElmQID, ref: ElmReferenceElement, names: VisibleNames): Context? {
-        val name = ref.referenceName
-        val candidates = (names.global + names.imported)
-                .filter { it.name == name }
-                .mapNotNull { ModuleScope.getQualifierForName(file, it.moduleName, name) }
-                .filter { it != "" }
-        if (candidates.isEmpty()) return null
-        return Context(candidates, name, qid)
+        private fun makeContext(file: ElmFile, qid: ElmQID, ref: ElmReferenceElement, names: VisibleNames): Context? {
+            val name = ref.referenceName
+            val candidates = (names.global + names.imported)
+                    .filter { it.name == name }
+                    .mapNotNull { ModuleScope.getQualifierForName(file, it.moduleName, name) }
+                    .filter { it != "" }
+            if (candidates.isEmpty()) return null
+            return Context(candidates, name, qid)
+        }
     }
 
-    override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
-        if (file !is ElmFile) return
-        val context = findApplicableContext() ?: return
+    override fun applyFix(element: PsiElement, project: Project, descriptor: ProblemDescriptor) {
+        val context = findApplicableContext(element) ?: return
 
         when (context.candidates.size) {
             0 -> error("should not happen: must be at least one candidate")

Fix memory leaks in quick fixes (#596)