diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8e7dcbf..269fef6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Added structural conversion hints for Kotlin: with the caret on an operator or convention-named call you can now convert `a.plus(b)` to `a + b`, `a.get(i)`/`a.set(i, v)` to `a[i]`/`a[i] = v`, `a.contains(b)` to `b in a`, `a.invoke(b)` to `a(b)`, `a.not()`/`a.unaryPlus()` to `!a`/`+a`, and `f() {}` to `f {}` directly from the hint popup.
# 0.11.54 (2026-10-12)
- Added **New → Kotlin Package** for **Source Packages**, **Test Packages**, **Kotlin Source Packages**, and **Kotlin Test Packages**, creating the requested nested package hierarchy exclusively beneath the selected source or test root in Maven, Gradle, and Ant/J2SE projects.
diff --git a/CoreImpl/pom.xml b/CoreImpl/pom.xml
index 8feb779a..d1390a6a 100644
--- a/CoreImpl/pom.xml
+++ b/CoreImpl/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-core-impl
diff --git a/KotlinCodeInsightImpl/pom.xml b/KotlinCodeInsightImpl/pom.xml
new file mode 100644
index 00000000..80b1ad9b
--- /dev/null
+++ b/KotlinCodeInsightImpl/pom.xml
@@ -0,0 +1,318 @@
+
+
+ 4.0.0
+
+ io.github.nbplugins
+ netbeans-plugin-kotlin
+ 0.11.284-SNAPSHOT
+ ../pom.xml
+
+ netbeans-plugin-kotlin-code-insight-impl
+ jar
+ Netbeans Plugin Kotlin Code Insight Impl
+
+ Structural-conversion inspections and intentions compiled from
+ submodules/IntellijCommunity (code-insight/inspections-k2,
+ code-insight/intentions-k2, code-insight/inspections-shared).
+ These are the IDEA implementations of operator/function conversions
+ (a + b <-> a.plus(b), a[i] <-> a.get(i), a in b <-> b.contains(a),
+ f {} <-> f() {}) consumed by the NetBeans adapter layer in the Nbm module.
+
+
+
+
+ ${project.build.directory}/generated-sources/code-insight
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+
+
+
+
+ copy-inspections-k2
+ initialize
+ copy-resources
+
+ ${project.build.directory}/generated-sources/code-insight
+ true
+
+
+ ${basedir}/../submodules/IntellijCommunity/plugins/kotlin/code-insight/inspections-k2/src
+ false
+
+ org/jetbrains/kotlin/idea/k2/codeinsight/inspections/expressions/ReplaceCallWithBinaryOperatorInspection.kt
+ org/jetbrains/kotlin/idea/k2/codeinsight/inspections/ReplaceGetOrSetInspection.kt
+ org/jetbrains/kotlin/idea/k2/codeinsight/inspections/ReplaceContainsInspection.kt
+
+
+
+
+
+
+
+
+ copy-intentions-k2
+ initialize
+ copy-resources
+
+ ${project.build.directory}/generated-sources/code-insight
+ true
+
+
+ ${basedir}/../submodules/IntellijCommunity/plugins/kotlin/code-insight/intentions-k2/src
+ false
+
+ org/jetbrains/kotlin/idea/k2/codeinsight/intentions/ReplaceInvokeIntention.kt
+ org/jetbrains/kotlin/idea/k2/codeinsight/intentions/ReplaceCallWithUnaryOperatorIntention.kt
+
+
+
+
+
+
+
+
+ copy-inspections-shared
+ initialize
+ copy-resources
+
+ ${project.build.directory}/generated-sources/code-insight
+ true
+
+
+ ${basedir}/../submodules/IntellijCommunity/plugins/kotlin/code-insight/inspections-shared/src
+ false
+
+ org/jetbrains/kotlin/idea/codeInsight/inspections/shared/RemoveEmptyParenthesesFromLambdaCallInspection.kt
+
+
+
+
+
+
+
+
+ copy-local-code-insight
+ initialize
+ copy-resources
+
+ ${project.build.directory}/generated-sources/code-insight
+ true
+
+
+ ${basedir}/src/main/kotlin
+ false
+
+
+
+
+
+
+
+
+
+
+ org.codehaus.gmavenplus
+ gmavenplus-plugin
+
+
+ patch-code-insight-impl
+ generate-sources
+ execute
+
+
+
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+ ${kotlin.runtime.languageVersion}
+ ${kotlin.runtime.languageVersion}
+ 17
+
+ -Xskip-metadata-version-check
+ -Xcontext-parameters
+ -opt-in=kotlin.ExperimentalContextParameters
+ -opt-in=kotlin.RequiresOptIn
+ -opt-in=org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+ -opt-in=org.jetbrains.kotlin.analysis.api.KaIdeApi
+ -opt-in=org.jetbrains.kotlin.analysis.api.KaContextParameterApi
+
+
+ ${project.build.directory}/generated-sources/code-insight
+
+
+
+
+ compile
+ process-sources
+ compile
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ -proc:none
+
+
+
+ default-compile
+ compile
+ compile
+
+
+ ${project.build.directory}/generated-sources/code-insight
+
+
+
+
+
+
+
+
+
+
+
+ com.jetbrains.intellij.platform
+ core
+ provided
+
+
+ com.jetbrains.intellij.platform
+ analysis
+ provided
+
+
+ com.jetbrains.intellij.platform
+ extensions
+ provided
+
+
+ com.jetbrains.intellij.platform
+ core-impl
+ 193.7288.26
+ provided
+
+ **
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-compiler-ir-for-ide
+ provided
+
+ **
+
+
+
+ org.jetbrains.kotlin
+ kotlin-compiler-common-for-ide
+ provided
+
+ **
+
+
+
+
+
+ org.jetbrains.kotlin
+ analysis-api-for-ide
+ provided
+
+ **
+
+
+
+ org.jetbrains.kotlin
+ analysis-api-impl-base-for-ide
+ provided
+
+ **
+
+
+
+
+
+ com.jetbrains.intellij.java
+ java-psi
+ ${intellij.platform.253.version}
+ provided
+
+ **
+
+
+
+
+
+ io.github.nbplugins
+ netbeans-plugin-kotlin-fixes-impl
+
+
+
+
+ io.github.nbplugins
+ netbeans-plugin-kotlin-intention-utils
+
+
+
+
+ io.github.nbplugins
+ netbeans-plugin-kotlin-refactoring
+
+
+
+ org.jetbrains
+ annotations
+ provided
+
+
+
diff --git a/KotlinCompilerCliBase/pom.xml b/KotlinCompilerCliBase/pom.xml
index 6bb4d10c..f77043d0 100644
--- a/KotlinCompilerCliBase/pom.xml
+++ b/KotlinCompilerCliBase/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-compiler-cli-base
diff --git a/KotlinFixesImpl/pom.xml b/KotlinFixesImpl/pom.xml
index 07c8e769..36f0c5a4 100644
--- a/KotlinFixesImpl/pom.xml
+++ b/KotlinFixesImpl/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-fixes-impl
@@ -105,6 +105,31 @@
+
+
+ copy-codeinsight-api-framework
+ initialize
+ copy-resources
+
+ ${project.build.directory}/generated-sources/fixes
+ true
+
+
+ ${basedir}/../submodules/IntellijCommunity/plugins/kotlin/code-insight/api/src
+ false
+
+ org/jetbrains/kotlin/idea/codeinsight/api/applicators/ApplicabilityRange.kt
+ org/jetbrains/kotlin/idea/codeinsight/api/applicable/ApplicableRangesProvider.kt
+ org/jetbrains/kotlin/idea/codeinsight/api/applicable/ContextProvider.kt
+ org/jetbrains/kotlin/idea/codeinsight/api/applicable/inspections/KotlinApplicableInspectionBase.kt
+ org/jetbrains/kotlin/idea/codeinsight/api/applicable/inspections/KotlinModCommandQuickFix.kt
+ org/jetbrains/kotlin/idea/codeinsight/api/applicable/intentions/KotlinApplicableModCommandAction.kt
+
+
+
+
+
+
copy-local-fixes
@@ -152,25 +177,20 @@ def patches = [
new_: 'public abstract fun invoke('],
]
-// Remove ClassBased and Contextless inner classes from KotlinPsiUpdateModCommandAction —
-// we only use ElementBased. ClassBased imports ContextProvider/getElementContext/KtElement
-// which are not in our classpath; Contextless also uses KtElement.
+// Remove the Contextless inner class from KotlinPsiUpdateModCommandAction —
+// we only use ElementBased and ClassBased. ClassBased is kept because
+// KotlinApplicableModCommandAction extends it; its ContextProvider /
+// getElementContext / KtElement references are now satisfiable because
+// ContextProvider.kt is compiled in this module (see the patch section below).
def modCommandFile = new File(srcDir,
'org/jetbrains/kotlin/idea/codeinsight/api/applicable/intentions/KotlinPsiUpdateModCommandAction.kt')
if (modCommandFile.exists()) {
def lines = modCommandFile.readLines()
def out = []
- def skipImports = [
- 'import org.jetbrains.kotlin.idea.codeinsight.api.applicable.ContextProvider',
- 'import org.jetbrains.kotlin.idea.codeinsight.api.applicable.getElementContext',
- 'import org.jetbrains.kotlin.psi.KtElement',
- // Note: kotlin.reflect.KClass is kept — used in the sealed class constructor signature
- ]
- def skipClassMarkers = ['abstract class ClassBased', 'abstract class Contextless']
+ def skipClassMarkers = ['abstract class Contextless']
def inSkippedClass = false
def braceDepth = 0
lines.each { line ->
- if (skipImports.any { line.startsWith(it) }) return
if (!inSkippedClass && skipClassMarkers.any { line.contains(it) }) {
inSkippedClass = true
braceDepth = 0
@@ -183,7 +203,7 @@ if (modCommandFile.exists()) {
out << line
}
modCommandFile.text = out.join('\n')
- log.info("Removed ClassBased and Contextless from KotlinPsiUpdateModCommandAction.kt")
+ log.info("Removed Contextless from KotlinPsiUpdateModCommandAction.kt")
}
patches.each { p ->
@@ -256,6 +276,91 @@ if (addReturnExprFile.exists()) {
addReturnExprFile.text = text
log.info("Patched AddReturnExpressionFix.kt: removed safeAs/endOffset, simplified invoke body")
}
+
+// ---------------------------------------------------------------------------
+// code-insight/api applicable/** framework (used by the K2 inspections and
+// intentions in the KotlinCodeInsightImpl module).
+//
+// The framework is compiled standalone here, without the surrounding
+// code-insight modules. Two kinds of references are absent from this
+// classpath and are patched out:
+// * org.jetbrains.kotlin.idea.codeinsight.api.applicable.extensions.*
+// (KaDanglingFileResolutionModeProvider) and analyzeCopy / copyOrigin —
+// only needed for injected/dangling files, which this standalone
+// container never produces.
+// * org.jetbrains.kotlin.analysis.api.permissions.forbidAnalysis —
+// a development-time assertion helper; inlining its body keeps the
+// "no analysis on EDT" guard semantics without the dependency.
+// ---------------------------------------------------------------------------
+
+// ApplicabilityRange.kt: textRangeIn lives in base/psi, which is not a dependency here.
+def applicabilityRangeFile = new File(srcDir,
+ 'org/jetbrains/kotlin/idea/codeinsight/api/applicators/ApplicabilityRange.kt')
+if (applicabilityRangeFile.exists()) {
+ def text = applicabilityRangeFile.text
+ text = text.replace('import org.jetbrains.kotlin.idea.base.psi.textRangeIn\n', '')
+ text = text.replace('it.textRangeIn(element)',
+ 'com.intellij.openapi.util.TextRange(it.textRange.startOffset - element.textRange.startOffset, it.textRange.endOffset - element.textRange.startOffset)')
+ applicabilityRangeFile.text = text
+ log.info("Patched ApplicabilityRange.kt: inlined textRangeIn")
+}
+
+// ContextProvider.kt: drop analyzeCopy/copyOrigin (injected-file support).
+def contextProviderFile = new File(srcDir,
+ 'org/jetbrains/kotlin/idea/codeinsight/api/applicable/ContextProvider.kt')
+if (contextProviderFile.exists()) {
+ def text = contextProviderFile.text
+ text = text.replace('import org.jetbrains.kotlin.analysis.api.analyzeCopy\n', '')
+ text = text.replace('import org.jetbrains.kotlin.analysis.api.projectStructure.copyOrigin\n', '')
+ text = text.replace('import org.jetbrains.kotlin.idea.codeinsight.api.applicable.extensions.KaDanglingFileResolutionModeProvider\n', '')
+ text = text.replace(
+ '''@OptIn(KaExperimentalApi::class)
+fun ContextProvider.getElementContext(
+ element: E,
+): C? = if (element.containingFile.copyOrigin == null) analyze(element) {
+ prepareContext(element)
+} else analyzeCopy(
+ element,
+ KaDanglingFileResolutionModeProvider.getDanglingFileResolutionMode(element)
+) {
+ prepareContext(element)
+}''',
+ '''@OptIn(KaExperimentalApi::class)
+fun ContextProvider.getElementContext(
+ element: E,
+): C? = analyze(element) {
+ prepareContext(element)
+}''')
+ contextProviderFile.text = text
+ log.info("Patched ContextProvider.kt: dropped dangling-file support")
+}
+
+// KotlinApplicableInspectionBase.kt: inline forbidAnalysis.
+def applicableInspectionFile = new File(srcDir,
+ 'org/jetbrains/kotlin/idea/codeinsight/api/applicable/inspections/KotlinApplicableInspectionBase.kt')
+if (applicableInspectionFile.exists()) {
+ def text = applicableInspectionFile.text
+ text = text.replace('import org.jetbrains.kotlin.analysis.api.permissions.forbidAnalysis\n', '')
+ text = text.replace('forbidAnalysis("isApplicableByPsi") {', 'run {')
+ text = text.replace('forbidAnalysis("getApplicabilityRanges") {', 'run {')
+ applicableInspectionFile.text = text
+ log.info("Patched KotlinApplicableInspectionBase.kt: inlined forbidAnalysis")
+}
+
+// KotlinApplicableModCommandAction.kt: inline forbidAnalysis and widen isElementApplicable.
+// `isElementApplicable` is `protected final` in the IDEA source; the NetBeans adapter layer
+// lives in a different module and must call it to drive applicability, so it is made public.
+def applicableModCommandFile = new File(srcDir,
+ 'org/jetbrains/kotlin/idea/codeinsight/api/applicable/intentions/KotlinApplicableModCommandAction.kt')
+if (applicableModCommandFile.exists()) {
+ def text = applicableModCommandFile.text
+ text = text.replace('import org.jetbrains.kotlin.analysis.api.permissions.forbidAnalysis\n', '')
+ text = text.replace('forbidAnalysis("isApplicableByPsi") {', 'run {')
+ text = text.replace('forbidAnalysis("getApplicabilityRanges") {', 'run {')
+ text = text.replace('\n final override fun isElementApplicable(', '\n public final override fun isElementApplicable(')
+ applicableModCommandFile.text = text
+ log.info("Patched KotlinApplicableModCommandAction.kt: inlined forbidAnalysis, widened isElementApplicable")
+}
]]>
diff --git a/KotlinFormatter/pom.xml b/KotlinFormatter/pom.xml
index 1fb0f873..6f399a52 100644
--- a/KotlinFormatter/pom.xml
+++ b/KotlinFormatter/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-formatter
diff --git a/KotlinHighlighting/pom.xml b/KotlinHighlighting/pom.xml
index 12d4863e..e3b7b97c 100644
--- a/KotlinHighlighting/pom.xml
+++ b/KotlinHighlighting/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-highlighting
diff --git a/KotlinIcons/pom.xml b/KotlinIcons/pom.xml
index f0c9d688..c25cecf3 100644
--- a/KotlinIcons/pom.xml
+++ b/KotlinIcons/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-icons
diff --git a/KotlinIntentionUtils/pom.xml b/KotlinIntentionUtils/pom.xml
index 547aad73..be365f8a 100644
--- a/KotlinIntentionUtils/pom.xml
+++ b/KotlinIntentionUtils/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-intention-utils
diff --git a/KotlinRefactoring/pom.xml b/KotlinRefactoring/pom.xml
index ee79aced..6faa1a81 100644
--- a/KotlinRefactoring/pom.xml
+++ b/KotlinRefactoring/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-refactoring
diff --git a/Nbm/pom.xml b/Nbm/pom.xml
index e5975fd2..25b876a7 100644
--- a/Nbm/pom.xml
+++ b/Nbm/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-nbm
@@ -259,6 +259,10 @@
io.github.nbplugins
netbeans-plugin-kotlin-refactoring
+
+ io.github.nbplugins
+ netbeans-plugin-kotlin-code-insight-impl
+
com.jetbrains.intellij.platform
analysis
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/KotlinHintsProvider.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/KotlinHintsProvider.kt
index 4a3a2996..7633f84c 100644
--- a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/KotlinHintsProvider.kt
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/KotlinHintsProvider.kt
@@ -100,6 +100,7 @@ import io.github.nbplugins.kotlin.nbm.hints.intentions.KaChangeVisibilityToPubli
import io.github.nbplugins.kotlin.nbm.hints.intentions.KaChangeVisibilityToPrivateIntention
import io.github.nbplugins.kotlin.nbm.hints.intentions.KaChangeVisibilityToProtectedIntention
import io.github.nbplugins.kotlin.nbm.hints.intentions.KaChangeVisibilityToInternalIntention
+import io.github.nbplugins.kotlin.nbm.hints.intentions.KaStructuralConversionHints
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.hints.KotlinRuleContext
import org.jetbrains.kotlin.hints.KotlinRule
@@ -202,7 +203,9 @@ class KotlinHintsProvider : HintsProvider {
KaChangeVisibilityToPublicIntention(doc, kaKtFile, psi),
KaChangeVisibilityToPrivateIntention(doc, kaKtFile, psi),
KaChangeVisibilityToProtectedIntention(doc, kaKtFile, psi),
- KaChangeVisibilityToInternalIntention(doc, kaKtFile, psi)
+ KaChangeVisibilityToInternalIntention(doc, kaKtFile, psi),
+ // Group F8 — Structural conversions (compiled IDEA inspections/intentions)
+ *KaStructuralConversionHints.hints(doc, kaKtFile, psi).toTypedArray()
).filter { it.isApplicable(psi.textRange.startOffset) }
}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/fixes/KaModCommandFix.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/fixes/KaModCommandFix.kt
index 0cafbc77..c204d613 100644
--- a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/fixes/KaModCommandFix.kt
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/fixes/KaModCommandFix.kt
@@ -4,11 +4,13 @@ package io.github.nbplugins.kotlin.nbm.hints.fixes
import com.intellij.modcommand.ActionContext
import com.intellij.modcommand.ModPsiUpdater
+import com.intellij.modcommand.PsiBasedModCommandAction
import com.intellij.modcommand.PsiUpdateModCommandAction
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import io.github.nbplugins.kotlin.nbm.diagnostics.KaDiagnosticError
import io.github.nbplugins.kotlin.nbm.hints.atomicChange
+import io.github.nbplugins.kotlin.nbm.hints.intentions.KaIdeaReflection
import org.jetbrains.kotlin.hints.KotlinRule
import org.jetbrains.kotlin.language.Priorities
import org.jetbrains.kotlin.psi.KtFile
@@ -74,34 +76,37 @@ class KaModCommandFix(
TextRange.EMPTY_RANGE,
element
)
- syncMutation(kaKtFile, doc) { callProtectedInvoke(ctx, NoOpModPsiUpdater) }
+ syncMutation(kaKtFile, doc) { callInvoke(ideaFix, ctx, element, NoOpModPsiUpdater) }
}
- /**
- * Calls `invoke(ActionContext, E, ModPsiUpdater)` on [ideaFix] via reflection.
- *
- * In era 253 the IntelliJ fixes extend [PsiUpdateModCommandAction] whose `invoke` is
- * `protected abstract` — not directly callable from outside the hierarchy. Reflection
- * is required. We walk the class hierarchy looking for the first non-bridge, non-synthetic
- * `invoke` with three parameters; the concrete override in each fix subclass is found first.
- */
- private fun callProtectedInvoke(ctx: ActionContext, updater: ModPsiUpdater) {
- var cls: Class<*>? = ideaFix.javaClass
- while (cls != null) {
- val m = cls.declaredMethods.firstOrNull { m ->
- m.name == "invoke" && m.parameterCount == 3 && !m.isSynthetic && !m.isBridge
- }
- if (m != null) {
- m.isAccessible = true
- m.invoke(ideaFix, ctx, element, updater)
- return
- }
- cls = cls.superclass
+ companion object {
+ /**
+ * Invokes the `invoke(ActionContext, E, ModPsiUpdater)` method of [action] via reflection.
+ *
+ * In era 253 the IntelliJ fixes extend [PsiUpdateModCommandAction] whose `invoke` is
+ * `protected abstract` — not directly callable from outside the hierarchy, so
+ * [KaIdeaReflection] walks the class hierarchy for the first non-bridge, non-synthetic
+ * three-parameter `invoke`; the concrete override in each fix subclass is found first.
+ *
+ * The parameter is [PsiBasedModCommandAction] rather than [PsiUpdateModCommandAction]
+ * because the Kotlin plugin's own `KotlinPsiUpdateModCommandAction` — the base of the
+ * compiled IDEA intentions — derives directly from the former.
+ *
+ * @param action the action whose `invoke` should run
+ * @param ctx the action context to pass
+ * @param element the target PSI element
+ * @param updater the updater to pass
+ */
+ @JvmStatic
+ fun callInvoke(
+ action: PsiBasedModCommandAction,
+ ctx: ActionContext,
+ element: E,
+ updater: ModPsiUpdater,
+ ) {
+ KaIdeaReflection.callOrFail(action, "invoke", 3, ctx, element, updater)
}
- error("Cannot find invoke(ActionContext, E, ModPsiUpdater) on ${ideaFix.javaClass}")
- }
- companion object {
/**
* Applies [mutate] as a PSI mutation and syncs the result to [doc].
*
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaInspectionHint.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaInspectionHint.kt
new file mode 100644
index 00000000..ff00c181
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaInspectionHint.kt
@@ -0,0 +1,116 @@
+/*******************************************************************************
+ * Copyright 2026 nbplugins contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *******************************************************************************/
+package io.github.nbplugins.kotlin.nbm.hints.intentions
+
+import com.intellij.modcommand.ModPsiUpdater
+import com.intellij.openapi.project.Project
+import io.github.nbplugins.kotlin.nbm.hints.KaApplicableIntention
+import io.github.nbplugins.kotlin.nbm.hints.fixes.KaModCommandFix
+import io.github.nbplugins.kotlin.nbm.hints.fixes.NoOpModPsiUpdater
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.idea.codeinsight.api.applicable.getElementContext
+import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase
+import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinModCommandQuickFix
+import org.jetbrains.kotlin.psi.KtElement
+import org.jetbrains.kotlin.psi.KtFile
+import javax.swing.text.Document
+
+/**
+ * Drives an IDEA [KotlinApplicableInspectionBase] inspection compiled from
+ * `submodules/IntellijCommunity` as a NetBeans caret hint.
+ *
+ * IDEA inspections are normally run by the platform's inspection engine, which builds a
+ * `ProblemsHolder`, calls the inspection's visitor, and surfaces the resulting
+ * `ProblemDescriptor` and its quick fixes in the Alt+Enter menu. NetBeans has no such
+ * engine for Kotlin — hints are caret-driven — so this adapter runs the inspection's
+ * applicability pipeline directly, in the same order its own `visitTargetElement` does:
+ *
+ * 1. `isApplicableByPsi` — cheap PSI-only pre-filter.
+ * 2. `getApplicableRanges` — element-relative ranges; the caret must fall inside one.
+ * 3. `getElementContext` — the K2 Analysis API pass returning the inspection's context,
+ * or null when the inspection is not applicable.
+ * 4. `createQuickFix` — the fix to offer, reached by reflection because the inspection
+ * declares it `protected` (the same technique [KaModCommandFix] uses for `invoke`).
+ *
+ * Applying the fix calls `KotlinModCommandQuickFix.applyFix(Project, E, ModPsiUpdater)`,
+ * also `protected`, again by reflection.
+ *
+ * @param inspection the compiled IDEA inspection to adapt
+ * @param doc the Swing document for the file being edited
+ * @param kaKtFile K2-session-owned [KtFile] for this file
+ * @param psi PSI element at the caret position, belonging to [kaKtFile]
+ */
+class KaIdeaInspectionHint(
+ private val inspection: KotlinApplicableInspectionBase,
+ doc: Document,
+ kaKtFile: KtFile,
+ private val element: E,
+) : KaApplicableIntention(doc, kaKtFile, element) {
+
+ /**
+ * The fix resolved during the last [isApplicable] call.
+ *
+ * Resolution costs a K2 Analysis API pass, so it is cached rather than repeated in
+ * [implement]. NetBeans calls [isApplicable] before [implement] on the same instance.
+ */
+ private var resolvedFix: KotlinModCommandQuickFix? = null
+
+ /**
+ * Reports whether the IDEA inspection applies at [caretOffset].
+ *
+ * The inspection's own K2 checks run inside [runCatching]: they are the only path in the
+ * hint provider that reaches a compiled IDEA inspection by reflection, so a failure here
+ * must not take down the whole caret popup. An inspection that cannot be evaluated is
+ * simply not offered.
+ */
+ override fun isApplicable(caretOffset: Int): Boolean = runCatching {
+ if (!inspection.isApplicableByPsi(element)) return@runCatching false
+
+ val relativeCaretOffset = caretOffset - element.textRange.startOffset
+ if (inspection.getApplicableRanges(element).none { it.containsOffset(relativeCaretOffset) }) {
+ return@runCatching false
+ }
+
+ val fix = resolveQuickFix() ?: return@runCatching false
+ resolvedFix = fix
+ true
+ }.getOrDefault(false)
+
+ override fun getDescription(): String = resolvedFix?.name ?: inspection.javaClass.simpleName
+
+ override fun implement() {
+ val fix = resolvedFix ?: return
+ KaModCommandFix.syncMutation(kaKtFile, doc) {
+ @Suppress("UNCHECKED_CAST")
+ val updater = NoOpModPsiUpdater as ModPsiUpdater
+ KaIdeaReflection.call(fix, "applyFix", 3, element.project as Project, element, updater)
+ }
+ }
+
+ /**
+ * Runs the inspection's context and quick-fix resolution.
+ *
+ * @return the resolved fix, or null when the inspection is not applicable here
+ */
+ @OptIn(KaExperimentalApi::class)
+ private fun resolveQuickFix(): KotlinModCommandQuickFix? {
+ val context = inspection.getElementContext(element) ?: return null
+ @Suppress("UNCHECKED_CAST")
+ return KaIdeaReflection.call(inspection, "createQuickFix", 2, element, context)
+ as? KotlinModCommandQuickFix
+ }
+}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaIntentionHint.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaIntentionHint.kt
new file mode 100644
index 00000000..bdd73c95
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaIntentionHint.kt
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright 2026 nbplugins contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *******************************************************************************/
+package io.github.nbplugins.kotlin.nbm.hints.intentions
+
+import com.intellij.modcommand.ActionContext
+import com.intellij.modcommand.Presentation
+import com.intellij.openapi.util.TextRange
+import io.github.nbplugins.kotlin.nbm.hints.KaApplicableIntention
+import io.github.nbplugins.kotlin.nbm.hints.fixes.KaModCommandFix
+import io.github.nbplugins.kotlin.nbm.hints.fixes.NoOpModPsiUpdater
+import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.KotlinApplicableModCommandAction
+import org.jetbrains.kotlin.psi.KtElement
+import org.jetbrains.kotlin.psi.KtFile
+import javax.swing.text.Document
+
+/**
+ * Drives an IDEA [KotlinApplicableModCommandAction] intention compiled from
+ * `submodules/IntellijCommunity` as a NetBeans caret hint.
+ *
+ * IDEA decides applicability in [KotlinApplicableModCommandAction.isElementApplicable], which
+ * needs an [ActionContext] carrying the absolute caret offset. This adapter builds that context
+ * and delegates, so the IDEA implementation — including its K2 Analysis API checks — runs
+ * unchanged.
+ *
+ * Applying the fix goes through [KaModCommandFix], which invokes the action's
+ * `invoke(ActionContext, E, ModPsiUpdater)` by reflection and syncs the mutated PSI back to
+ * the document.
+ *
+ * @param action the compiled IDEA intention to adapt
+ * @param doc the Swing document for the file being edited
+ * @param kaKtFile K2-session-owned [KtFile] for this file
+ * @param psi PSI element at the caret position, belonging to [kaKtFile]
+ */
+class KaIdeaIntentionHint(
+ private val action: KotlinApplicableModCommandAction,
+ doc: Document,
+ kaKtFile: KtFile,
+ private val element: E,
+) : KaApplicableIntention(doc, kaKtFile, element) {
+
+ /**
+ * Reports whether the IDEA action applies at [caretOffset].
+ *
+ * The action's own K2 checks run inside [runCatching]: they are the only path in the hint
+ * provider that reaches a compiled IDEA action by reflection, so a failure here must not
+ * take down the whole caret popup. An action that cannot be evaluated is simply not offered.
+ */
+ override fun isApplicable(caretOffset: Int): Boolean = runCatching {
+ action.isElementApplicable(element, actionContext(caretOffset))
+ }.getOrDefault(false)
+
+ /**
+ * Returns the action's user-visible name.
+ *
+ * [KotlinApplicableModCommandAction] builds it in `getPresentation`, which is `protected`
+ * and only reachable by reflection; when no presentation is produced the family name is
+ * the fallback, mirroring IDEA's own behaviour.
+ */
+ override fun getDescription(): String {
+ val presentation = KaIdeaReflection.call(
+ action, "getPresentation", 2, actionContext(element.textRange.startOffset), element
+ ) as? Presentation
+ return presentation?.name() ?: action.familyName
+ }
+
+ override fun implement() {
+ val ctx = actionContext(element.textRange.startOffset)
+ // KotlinPsiUpdateModCommandAction declares
+ // invoke(ActionContext, E, C, ModPsiUpdater) — four parameters, with the element
+ // context that isElementApplicable computed and discarded. It is resolved again here
+ // through the action's own protected getElementContext(ActionContext, E).
+ val elementContext = KaIdeaReflection.call(action, "getElementContext", 2, ctx, element)
+ KaModCommandFix.syncMutation(kaKtFile, doc) {
+ KaIdeaReflection.call(action, "invoke", 4, ctx, element, elementContext, NoOpModPsiUpdater)
+ }
+ }
+
+ /**
+ * Builds the [ActionContext] IDEA expects for [caretOffset].
+ *
+ * @param caretOffset absolute caret offset in the document
+ */
+ private fun actionContext(caretOffset: Int) = ActionContext(
+ element.project,
+ kaKtFile,
+ caretOffset,
+ TextRange.EMPTY_RANGE,
+ element
+ )
+}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflection.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflection.kt
new file mode 100644
index 00000000..0b71d986
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflection.kt
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * Copyright 2026 nbplugins contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *******************************************************************************/
+package io.github.nbplugins.kotlin.nbm.hints.intentions
+
+/**
+ * Reflection helper for reaching the members IDEA's inspection/intention API declares
+ * `protected`.
+ *
+ * The compiled IDEA classes are consumed from another module, so Kotlin's visibility rules
+ * hide their `protected` members — `getPresentation`, `createQuickFix`, `applyFix`. The
+ * NetBeans adapter must drive exactly those members to reuse the IDEA behaviour instead of
+ * reimplementing it. Looking the member up by name and arity across the class hierarchy
+ * also skips the compiler-generated bridge and synthetic methods that share the name.
+ */
+internal object KaIdeaReflection {
+
+ /**
+ * Invokes the first declared method named [name] with [parameterCount] parameters found
+ * walking [target]'s class hierarchy from the most-derived class upwards.
+ *
+ * @param target receiver of the invocation
+ * @param name method name to look for
+ * @param parameterCount required number of parameters (bridges and synthetics excluded)
+ * @param args arguments to pass
+ * @return the method's return value, or null when no matching method exists
+ */
+ fun call(target: Any, name: String, parameterCount: Int, vararg args: Any?): Any? =
+ find(target, name, parameterCount)?.let { method ->
+ method.isAccessible = true
+ method.invoke(target, *args)
+ }
+
+ /**
+ * Invokes [name] like [call], but fails loudly when no matching method exists.
+ *
+ * Use this when a missing member means the adapter is broken rather than the action simply
+ * not applying. The return value is still nullable: a Kotlin `Unit` function compiles to a
+ * `void` method, whose [java.lang.reflect.Method.invoke] result is always null.
+ *
+ * @param target receiver of the invocation
+ * @param name method name to look for
+ * @param parameterCount required number of parameters
+ * @param args arguments to pass
+ * @return the method's return value
+ */
+ fun callOrFail(target: Any, name: String, parameterCount: Int, vararg args: Any?): Any? {
+ val method = find(target, name, parameterCount)
+ ?: error("Cannot find $name/${parameterCount}args on ${target.javaClass}")
+ method.isAccessible = true
+ return method.invoke(target, *args)
+ }
+
+ /**
+ * Finds the first declared method named [name] taking [parameterCount] parameters.
+ *
+ * @param target receiver whose class hierarchy is searched
+ * @param name method name to look for
+ * @param parameterCount required number of parameters
+ * @return the method, or null when the hierarchy declares none
+ */
+ private fun find(target: Any, name: String, parameterCount: Int): java.lang.reflect.Method? {
+ var cls: Class<*>? = target.javaClass
+ while (cls != null) {
+ val method = cls.declaredMethods.firstOrNull { m ->
+ m.name == name && m.parameterCount == parameterCount && !m.isSynthetic && !m.isBridge
+ }
+ if (method != null) return method
+ cls = cls.superclass
+ }
+ return null
+ }
+}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaStructuralConversionHints.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaStructuralConversionHints.kt
new file mode 100644
index 00000000..e627e42e
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaStructuralConversionHints.kt
@@ -0,0 +1,100 @@
+/*******************************************************************************
+ * Copyright 2026 nbplugins contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *******************************************************************************/
+package io.github.nbplugins.kotlin.nbm.hints.intentions
+
+import com.intellij.psi.PsiElement
+import io.github.nbplugins.kotlin.nbm.hints.KaApplicableIntention
+import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.RemoveEmptyParenthesesFromLambdaCallInspection
+import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceContainsInspection
+import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceGetOrSetInspection
+import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.expressions.ReplaceCallWithBinaryOperatorInspection
+import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ReplaceCallWithUnaryOperatorIntention
+import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ReplaceInvokeIntention
+import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.KtValueArgumentList
+import javax.swing.text.Document
+
+/**
+ * Builds the F8 structural-conversion hints from the IDEA inspections and intentions
+ * compiled by the `KotlinCodeInsightImpl` module.
+ *
+ * Each IDEA action is typed on a specific PSI element, so the element the caret refers to
+ * must be looked up per conversion. These are the operator/function conversions:
+ *
+ * - `a.plus(b)` <-> `a + b`
+ * - `a.get(i)` / `a.set(i, v)` <-> `a[i]` / `a[i] = v`
+ * - `a.contains(b)` <-> `b in a`
+ * - `a.invoke(b)` <-> `a(b)`
+ * - `a.not()` <-> `!a`
+ * - `f() {}` <-> `f {}`
+ */
+object KaStructuralConversionHints {
+
+ /**
+ * Returns the structural-conversion hints applicable at the element [psi].
+ *
+ * @param doc the Swing document for the file being edited
+ * @param kaKtFile K2-session-owned [KtFile] for this file
+ * @param psi PSI element at the caret position, belonging to [kaKtFile]
+ * @return hints for every conversion whose target element encloses [psi]
+ */
+ fun hints(doc: Document, kaKtFile: KtFile, psi: PsiElement): List = listOfNotNull(
+ psi.ancestorOfType()?.let {
+ KaIdeaIntentionHint(ReplaceInvokeIntention(), doc, kaKtFile, it)
+ },
+ psi.ancestorOfType()?.let {
+ KaIdeaIntentionHint(ReplaceCallWithUnaryOperatorIntention(), doc, kaKtFile, it)
+ },
+ psi.ancestorOfType()?.let {
+ KaIdeaInspectionHint(ReplaceCallWithBinaryOperatorInspection(), doc, kaKtFile, it)
+ },
+ psi.ancestorOfType()?.let {
+ KaIdeaInspectionHint(ReplaceGetOrSetInspection(), doc, kaKtFile, it)
+ },
+ psi.ancestorOfType()?.let {
+ KaIdeaInspectionHint(ReplaceContainsInspection(), doc, kaKtFile, it)
+ },
+ // The caret may sit just after the `)` that closes the argument list, so the
+ // preceding leaf is searched too — matching IDEA, which anchors this inspection
+ // on the argument list itself.
+ (psi.ancestorOfType() ?: psi.previousLeafOfType())?.let {
+ KaIdeaInspectionHint(RemoveEmptyParenthesesFromLambdaCallInspection(), doc, kaKtFile, it)
+ },
+ )
+
+ /** Walks up from [this] returning the first element of type [T], or null. */
+ private inline fun PsiElement.ancestorOfType(): T? {
+ var current: PsiElement? = this
+ while (current != null) {
+ if (current is T) return current
+ current = current.parent
+ }
+ return null
+ }
+
+ /**
+ * Walks up from the leaf preceding [this].
+ *
+ * The `f() {}` -> `f {}` inspection is anchored on the [KtValueArgumentList], which the
+ * caret can sit just outside of.
+ */
+ private fun PsiElement.previousLeafOfType(): KtValueArgumentList? {
+ val previous = com.intellij.psi.util.PsiTreeUtil.prevLeaf(this) ?: return null
+ return previous.ancestorOfType()
+ }
+}
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflectionTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflectionTest.kt
new file mode 100644
index 00000000..c836fd0f
--- /dev/null
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIdeaReflectionTest.kt
@@ -0,0 +1,126 @@
+/*******************************************************************************
+ * Copyright 2026 nbplugins contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ *******************************************************************************/
+package io.github.nbplugins.kotlin.nbm.hints.intentions
+
+import org.netbeans.junit.NbTestCase
+
+/**
+ * Unit tests for [KaIdeaReflection].
+ *
+ * The helper exists to reach `protected` members of the compiled IDEA inspection/intention
+ * classes, so the tests exercise the same class-hierarchy walk against a local hierarchy:
+ * a base declaration, a derived override, overloads distinguished by arity, and a
+ * compiler-generated bridge method that must be skipped.
+ */
+class KaIdeaReflectionTest : NbTestCase("KaIdeaReflectionTest") {
+
+ /** A member declared only in the base class is found by walking up the hierarchy. */
+ fun testCallFindsBaseClassMember() {
+ assertEquals("base", KaIdeaReflection.call(Derived(), "baseOnly", 0))
+ }
+
+ /** The most-derived override wins over the base declaration. */
+ fun testCallPrefersMostDerivedOverride() {
+ assertEquals("derived", KaIdeaReflection.call(Derived(), "overridden", 0))
+ }
+
+ /** Overloads are selected by parameter count. */
+ fun testCallSelectsByArity() {
+ assertEquals("one:7", KaIdeaReflection.call(Overloaded(), "describe", 1, 7))
+ assertEquals("two:7:8", KaIdeaReflection.call(Overloaded(), "describe", 2, 7, 8))
+ }
+
+ /** Compiler-generated bridge methods are skipped, so the real override is invoked. */
+ fun testCallSkipsBridgeMethod() {
+ assertEquals("boxed", KaIdeaReflection.call(StringBox(), "unwrap", 0))
+ }
+
+ /** A missing member returns null instead of throwing. */
+ fun testCallReturnsNullWhenAbsent() {
+ assertNull(KaIdeaReflection.call(Derived(), "doesNotExist", 0))
+ }
+
+ /** A member whose parameter count does not match any overload returns null. */
+ fun testCallReturnsNullOnArityMismatch() {
+ assertNull(KaIdeaReflection.call(Overloaded(), "describe", 3, 1, 2, 3))
+ }
+
+ /** A `protected` member is reachable, which is the whole point of the helper. */
+ fun testCallReachesProtectedMember() {
+ assertEquals("protected", KaIdeaReflection.call(Derived(), "hidden", 0))
+ }
+
+ /** [KaIdeaReflection.callOrFail] returns the value of an existing member. */
+ fun testCallOrFailReturnsValue() {
+ assertEquals("derived", KaIdeaReflection.callOrFail(Derived(), "overridden", 0))
+ }
+
+ /** [KaIdeaReflection.callOrFail] returns null for a `void` method, which is a success. */
+ fun testCallOrFailReturnsNullForVoidMethod() {
+ assertNull(KaIdeaReflection.callOrFail(VoidMethod(), "run", 0))
+ }
+
+ /** [KaIdeaReflection.callOrFail] fails loudly when the member does not exist. */
+ fun testCallOrFailThrowsWhenAbsent() {
+ try {
+ KaIdeaReflection.callOrFail(Derived(), "doesNotExist", 0)
+ fail("Expected an IllegalStateException for a missing member")
+ } catch (expected: IllegalStateException) {
+ assertTrue(
+ "Message should name the missing member",
+ expected.message!!.contains("doesNotExist")
+ )
+ }
+ }
+
+ /** Base class declaring the members the derived class partly overrides. */
+ private open class Base {
+ protected fun hidden(): String = "protected"
+
+ fun baseOnly(): String = "base"
+
+ open fun overridden(): String = "base"
+ }
+
+ /** Derived class overriding one member and inheriting the others. */
+ private class Derived : Base() {
+ override fun overridden(): String = "derived"
+ }
+
+ /** Class with same-named overloads of different arity. */
+ private class Overloaded {
+ fun describe(a: Int): String = "one:$a"
+
+ fun describe(a: Int, b: Int): String = "two:$a:$b"
+ }
+
+ /** Class whose method returns `Unit`, compiled to a `void` method. */
+ private class VoidMethod {
+ fun run() {
+ }
+ }
+
+ /** Generic interface whose implementation gets a compiler-generated bridge method. */
+ private interface Box {
+ fun unwrap(): T
+ }
+
+ /** Implementation returning [String], producing a synthetic `unwrap(): Object` bridge. */
+ private class StringBox : Box {
+ override fun unwrap(): String = "boxed"
+ }
+}
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIntentionsTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIntentionsTest.kt
index bf1545de..bc5f986a 100644
--- a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIntentionsTest.kt
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/hints/intentions/KaIntentionsTest.kt
@@ -16,7 +16,18 @@
*******************************************************************************/
package io.github.nbplugins.kotlin.nbm.hints.intentions
+import com.intellij.psi.PsiElement
import io.github.nbplugins.kotlin.nbm.hints.KaIntentionTestBase
+import org.jetbrains.kotlin.idea.codeInsight.inspections.shared.RemoveEmptyParenthesesFromLambdaCallInspection
+import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase
+import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.KotlinApplicableModCommandAction
+import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceContainsInspection
+import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.ReplaceGetOrSetInspection
+import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.expressions.ReplaceCallWithBinaryOperatorInspection
+import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ReplaceCallWithUnaryOperatorIntention
+import org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ReplaceInvokeIntention
+import org.jetbrains.kotlin.psi.KtElement
+import java.lang.reflect.ParameterizedType
/**
* Integration tests for K2 [io.github.nbplugins.kotlin.nbm.hints.KaApplicableIntention] subclasses.
@@ -321,4 +332,142 @@ class KaIntentionsTest : KaIntentionTestBase("KaIntentions test", "intentions")
fun testKaChangeVisibilityToInternal() = doTest("kaChangeVisibilityToInternal") { doc, kaKtFile, psi ->
KaChangeVisibilityToInternalIntention(doc, kaKtFile, psi)
}
+
+ // ── F8 — Structural conversions (compiled IDEA sources) ─────────────────
+ //
+ // These adapt IDEA's own K2 inspections and intentions, compiled verbatim from
+ // submodules/IntellijCommunity by the KotlinCodeInsightImpl module. Unlike the
+ // hand-written intentions above, the element passed to the adapter must be the exact
+ // type the IDEA action targets, so it is found by walking up from the caret leaf.
+
+ /** F8: `a.plus(b)` -> `a + b` (IDEA ReplaceCallWithBinaryOperatorInspection). */
+ fun testKaReplaceCallWithBinaryOperator() = doTestInspection("kaReplaceCallWithBinaryOperator") {
+ ReplaceCallWithBinaryOperatorInspection()
+ }
+
+ /** F8: `a.get(i)` -> `a[i]` (IDEA ReplaceGetOrSetInspection). */
+ fun testKaReplaceGetOrSet() = doTestInspection("kaReplaceGetOrSet") {
+ ReplaceGetOrSetInspection()
+ }
+
+ /** F8: `a.contains(b)` -> `b in a` (IDEA ReplaceContainsInspection). */
+ fun testKaReplaceContains() = doTestInspection("kaReplaceContains") {
+ ReplaceContainsInspection()
+ }
+
+ /** F8: `f() {}` -> `f {}` (IDEA RemoveEmptyParenthesesFromLambdaCallInspection). */
+ fun testKaRemoveEmptyParenthesesFromLambdaCall() =
+ doTestInspection("kaRemoveEmptyParenthesesFromLambdaCall") {
+ RemoveEmptyParenthesesFromLambdaCallInspection()
+ }
+
+ /** F8: `a.invoke(b)` -> `a(b)` (IDEA ReplaceInvokeIntention). */
+ fun testKaReplaceInvoke() = doTestIntention("kaReplaceInvoke") { ReplaceInvokeIntention() }
+
+ /** F8: `a.not()` -> `!a` (IDEA ReplaceCallWithUnaryOperatorIntention). */
+ fun testKaReplaceCallWithUnaryOperator() =
+ doTestIntention("kaReplaceCallWithUnaryOperator") { ReplaceCallWithUnaryOperatorIntention() }
+
+ /**
+ * Runs an IDEA [KotlinApplicableInspectionBase] test through [KaIdeaInspectionHint].
+ *
+ * The caret leaf is walked up to the exact element type the inspection declares, because
+ * [KotlinApplicableInspectionBase] is typed on that element.
+ *
+ * @param fileName fixture name without extension
+ * @param factory creates the inspection under test
+ */
+ private fun doTestInspection(
+ fileName: String,
+ factory: () -> KotlinApplicableInspectionBase,
+ ) = doTest(fileName) { doc, kaKtFile, psi ->
+ val inspection = factory()
+ @Suppress("UNCHECKED_CAST")
+ KaIdeaInspectionHint(
+ inspection,
+ doc,
+ kaKtFile,
+ psi.targetElement(factoryElementClass(inspection)) as E
+ )
+ }
+
+ /**
+ * Runs an IDEA [KotlinApplicableModCommandAction] test through [KaIdeaIntentionHint].
+ *
+ * @param fileName fixture name without extension
+ * @param factory creates the intention under test
+ */
+ private fun doTestIntention(
+ fileName: String,
+ factory: () -> KotlinApplicableModCommandAction,
+ ) = doTest(fileName) { doc, kaKtFile, psi ->
+ val intention = factory()
+ @Suppress("UNCHECKED_CAST")
+ KaIdeaIntentionHint(
+ intention,
+ doc,
+ kaKtFile,
+ psi.targetElement(factoryElementClass(intention)) as E
+ )
+ }
+
+ /**
+ * Returns the [KtElement] class an IDEA action targets.
+ *
+ * Both applicable-inspection and applicable-intention bases expose their element type
+ * through the generic supertype's type argument; that is read here so the test does not
+ * have to restate it per fixture.
+ */
+ private fun factoryElementClass(action: Any): Class {
+ var cls: Class<*>? = action.javaClass
+ while (cls != null) {
+ val supertype = cls.genericSuperclass
+ if (supertype is ParameterizedType) {
+ val argument = supertype.actualTypeArguments.firstOrNull()
+ if (argument is Class<*> && KtElement::class.java.isAssignableFrom(argument)) {
+ @Suppress("UNCHECKED_CAST")
+ return argument as Class
+ }
+ }
+ cls = cls.superclass
+ }
+ error("Cannot determine target element class for ${action.javaClass}")
+ }
+
+ /** Walks up from [this] until an element of [type] is found. */
+ private fun PsiElement.ancestorOfType(type: Class<*>): KtElement {
+ var current: PsiElement? = this
+ while (current != null) {
+ if (type.isInstance(current)) {
+ return current as KtElement
+ }
+ current = current.parent
+ }
+ error("No ${type.simpleName} ancestor of ${text}")
+ }
+
+ /**
+ * Finds the element of [type] the caret refers to, searching both up and sideways.
+ *
+ * Most fixtures put the caret inside the target element, so an ancestor walk suffices.
+ * The `removeEmptyParenthesesFromLambdaCall` fixture is the exception: its caret sits
+ * just after `)` — outside the [org.jetbrains.kotlin.psi.KtValueArgumentList] the
+ * inspection targets — so the preceding leaf is walked up to instead.
+ */
+ private fun PsiElement.targetElement(type: Class<*>): KtElement {
+ ancestorOrNull(type)?.let { return it }
+ val previous = com.intellij.psi.util.PsiTreeUtil.prevLeaf(this)
+ return previous?.ancestorOrNull(type)
+ ?: error("No ${type.simpleName} near ${text}")
+ }
+
+ /** Walks up from [this] returning the first element of [type], or null. */
+ private fun PsiElement.ancestorOrNull(type: Class<*>): KtElement? {
+ var current: PsiElement? = this
+ while (current != null) {
+ if (type.isInstance(current)) return current as KtElement
+ current = current.parent
+ }
+ return null
+ }
}
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/RefactoringCoverageBaselineTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/RefactoringCoverageBaselineTest.kt
index 5a25cf63..57cb269a 100644
--- a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/RefactoringCoverageBaselineTest.kt
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/RefactoringCoverageBaselineTest.kt
@@ -39,7 +39,7 @@ class RefactoringCoverageBaselineTest : NbTestCase("RefactoringCoverageBaselineT
val records = Files.readAllLines(matrix)
.mapNotNull(::parseRecord)
assertEquals("Every coverage ID must occur exactly once", records.size, records.map { it.id }.toSet().size)
- assertEquals("F0 baseline row count", 24, records.size)
+ assertEquals("F0 baseline row count", 30, records.size)
records.forEach { record ->
assertTrue("Unknown status for ${record.id}: ${record.status}", record.status in STATUSES)
@@ -54,9 +54,9 @@ class RefactoringCoverageBaselineTest : NbTestCase("RefactoringCoverageBaselineT
}
}
- val totals = "**Baseline counts:** 0 complete, 24 partial, 0 absent, 24 total."
+ val totals = "**Baseline counts:** 0 complete, 30 partial, 0 absent, 30 total."
assertTrue("Visible baseline totals must match the records", Files.readString(matrix).contains(totals))
- assertEquals(24, records.count { it.status == "partial" })
+ assertEquals(30, records.count { it.status == "partial" })
assertEquals(0, records.count { it.status == "absent" })
}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.after b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.after
new file mode 100644
index 00000000..733cfee2
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.after
@@ -0,0 +1,5 @@
+package intentions
+
+fun kaRemoveEmptyParenthesesFromLambdaCall() {
+ listOf(1).forEach { }
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.caret
new file mode 100644
index 00000000..8b16f25e
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.caret
@@ -0,0 +1,5 @@
+package intentions
+
+fun kaRemoveEmptyParenthesesFromLambdaCall() {
+ listOf(1).forEach() { }
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.kt
new file mode 100644
index 00000000..626ffb3e
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaRemoveEmptyParenthesesFromLambdaCall.kt
@@ -0,0 +1,5 @@
+package intentions
+
+fun kaRemoveEmptyParenthesesFromLambdaCall() {
+ listOf(1).forEach() { }
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.after
new file mode 100644
index 00000000..b570d18e
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.after
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceCallWithBinaryOperator(): Int {
+ class Test
+ operator fun Test.plus(a: Int): Test = Test()
+ val test = Test()
+ return test + 1
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.caret
new file mode 100644
index 00000000..a714277a
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.caret
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceCallWithBinaryOperator(): Int {
+ class Test
+ operator fun Test.plus(a: Int): Test = Test()
+ val test = Test()
+ return test.plus(1)
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.kt
new file mode 100644
index 00000000..8a91f238
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithBinaryOperator.kt
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceCallWithBinaryOperator(): Int {
+ class Test
+ operator fun Test.plus(a: Int): Test = Test()
+ val test = Test()
+ return test.plus(1)
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.after
new file mode 100644
index 00000000..4467df26
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.after
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceCallWithUnaryOperator() {
+ class Test
+ operator fun Test.not(): Test = Test()
+ val test = Test()
+ !test
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.caret
new file mode 100644
index 00000000..89db21d6
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.caret
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceCallWithUnaryOperator() {
+ class Test
+ operator fun Test.not(): Test = Test()
+ val test = Test()
+ test.not()
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.kt
new file mode 100644
index 00000000..c14563a3
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceCallWithUnaryOperator.kt
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceCallWithUnaryOperator() {
+ class Test
+ operator fun Test.not(): Test = Test()
+ val test = Test()
+ test.not()
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.after
new file mode 100644
index 00000000..43c5046b
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.after
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceContains(): Boolean {
+ class Test
+ operator fun Test.contains(a: Int): Boolean = true
+ val test = Test()
+ return 1 in test
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.caret
new file mode 100644
index 00000000..37dbc556
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.caret
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceContains(): Boolean {
+ class Test
+ operator fun Test.contains(a: Int): Boolean = true
+ val test = Test()
+ return test.contains(1)
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.kt
new file mode 100644
index 00000000..8d000edd
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceContains.kt
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceContains(): Boolean {
+ class Test
+ operator fun Test.contains(a: Int): Boolean = true
+ val test = Test()
+ return test.contains(1)
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.after
new file mode 100644
index 00000000..043086c2
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.after
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceGetOrSet(): Int {
+ class Test
+ operator fun Test.get(i: Int): Int = 0
+ val test = Test()
+ return test[0]
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.caret
new file mode 100644
index 00000000..cbce7b33
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.caret
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceGetOrSet(): Int {
+ class Test
+ operator fun Test.get(i: Int): Int = 0
+ val test = Test()
+ return test.get(0)
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.kt
new file mode 100644
index 00000000..b79513ee
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceGetOrSet.kt
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceGetOrSet(): Int {
+ class Test
+ operator fun Test.get(i: Int): Int = 0
+ val test = Test()
+ return test.get(0)
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.after b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.after
new file mode 100644
index 00000000..c3f6ce21
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.after
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceInvoke() {
+ class Test
+ operator fun Test.invoke() {}
+ val test = Test()
+ test()
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.caret b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.caret
new file mode 100644
index 00000000..9d9d765a
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.caret
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceInvoke() {
+ class Test
+ operator fun Test.invoke() {}
+ val test = Test()
+ test.invoke()
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.kt b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.kt
new file mode 100644
index 00000000..d1f1feb7
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/intentions/kaReplaceInvoke.kt
@@ -0,0 +1,8 @@
+package intentions
+
+fun kaReplaceInvoke() {
+ class Test
+ operator fun Test.invoke() {}
+ val test = Test()
+ test.invoke()
+}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.after b/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.after
deleted file mode 100644
index 3f7e9bd5..00000000
--- a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.after
+++ /dev/null
@@ -1,4 +0,0 @@
-package intentions
-
-fun removeEmptyParenthesesFromLambdaCall(strs: Array) = strs.map {}
-
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.caret b/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.caret
deleted file mode 100644
index 26c907b2..00000000
--- a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.caret
+++ /dev/null
@@ -1,3 +0,0 @@
-package intentions
-
-fun removeEmptyParenthesesFromLambdaCall(strs: Array) = strs.map() {}
diff --git a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.kt b/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.kt
deleted file mode 100644
index a4d28441..00000000
--- a/Nbm/src/test/resources/projForTest/src/intentions/removeParenthesesFromLambdaCall.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package intentions
-
-fun removeEmptyParenthesesFromLambdaCall(strs: Array) = strs.map() {}
diff --git a/docs/development-plan.md b/docs/development-plan.md
index e96c70dd..80d4c455 100644
--- a/docs/development-plan.md
+++ b/docs/development-plan.md
@@ -1056,6 +1056,29 @@ Priority order: E1 → E2 → E3 → E4 → E5 → E6 → E7 → E8 → E9 → E
- Supports immediately invoked Kotlin lambdas and anonymous functions. Non-invoked functions,
Java/non-code/index-only references, and wider IDEA invocation/project-index cases remain unavailable.
+ - [x] **F8** — Structural conversions (operator/function conversions)
+ - Compiles the six IDEA K2 sources for the operator/function conversions into the new
+ `KotlinCodeInsightImpl` reactor module: `ReplaceCallWithBinaryOperatorInspection`,
+ `ReplaceGetOrSetInspection`, `ReplaceContainsInspection` (inspections-k2),
+ `ReplaceInvokeIntention`, `ReplaceCallWithUnaryOperatorIntention` (intentions-k2), and
+ `RemoveEmptyParenthesesFromLambdaCallInspection` (inspections-shared). The sources are copied
+ from `submodules/IntellijCommunity` by `maven-resources-plugin` and compiled unchanged; the
+ only patch is widening the three `internal class` inspections so the NetBeans layer can
+ construct them.
+ - `KotlinFixesImpl` was extended with the remaining `code-insight/api` applicable framework
+ (`KotlinApplicableInspectionBase`, `KotlinModCommandQuickFix`, `KotlinApplicableModCommandAction`,
+ `ApplicabilityRange`, `ApplicableRangesProvider`, `ContextProvider`) so those sources have
+ something to compile against.
+ - `KaIdeaIntentionHint` and `KaIdeaInspectionHint` drive the compiled actions by reflection
+ (their `getElementContext`/`createQuickFix`/`invoke`/`applyFix` are `protected`), reusing
+ `KaModCommandFix`'s `NoOpModPsiUpdater` and PSI→document sync. `KaStructuralConversionHints`
+ attaches each action to the PSI element the caret refers to, and
+ `KotlinHintsProvider.listOfK2Intentions` registers them alongside the hand-written hints.
+ - Covers `a.plus(b)` → `a + b`, `a.get(i)`/`a.set(i, v)` → `a[i]`/`a[i] = v`, `a.contains(b)` →
+ `b in a`, `a.invoke(b)` → `a(b)`, `a.not()`/`a.unaryPlus()` → `!a`/`+a`, and `f() {}` → `f {}`.
+ The reverse directions (`KaOperatorToFunctionIntention`, `KaConvertLambdaToReferenceIntention`)
+ stay hand-written: their IDEA counterparts drag in a much larger slice of `code-insight`.
+
- **E10** — J2K (Java→Kotlin): reimplement using `j2k/new` from `submodules/IntellijCommunity`
or binary artifact once published; wire up stubbed `Java2KotlinConverter` (stubbed since D2)
and re-enable `J2KTest`
diff --git a/docs/refactoring-coverage.md b/docs/refactoring-coverage.md
index 7718348e..ecc6073d 100644
--- a/docs/refactoring-coverage.md
+++ b/docs/refactoring-coverage.md
@@ -32,6 +32,12 @@ netbeans=; test=; milestone=
+
+
+
+
+
+
## Purpose and scope
@@ -48,7 +54,7 @@ The corresponding record comments above are deliberately machine-readable. `Refa
| **Absent** | IDEA exposes the Kotlin refactoring family but the NetBeans plugin has no corresponding command. |
| **Not applicable** | IDEA-only integration which is not a Kotlin refactoring target for this plugin. No such rows are currently tracked. |
-**Baseline counts:** 0 complete, 24 partial, 0 absent, 24 total.
+**Baseline counts:** 0 complete, 30 partial, 0 absent, 30 total.
## Current NetBeans refactorings
@@ -91,7 +97,31 @@ The corresponding record comments above are deliberately machine-readable. `Refa
| **F5** | Inline Type Alias and Inline Anonymous Function. |
| **F6** | Advanced Extract/Introduce and Extract Super parity. |
| **F7** | Full Rename/Safe Delete filesystem, automatic-renamer and Java-bridge coverage. |
-| **F8** | Structural conversions such as lambda/reference and operator/function conversions. |
+| **F8** | Structural conversions such as lambda/reference and operator/function conversions. Delivered by compiling the IDEA K2 inspections/intentions for the operator/function conversions into the `KotlinCodeInsightImpl` reactor module and driving them from the caret hint provider; see *Structural conversions* below. |
+
+## Structural conversions (F8)
+
+IDEA implements the operator/function conversions as K2 **inspections** and **intentions**. The
+`KotlinCodeInsightImpl` reactor module copies their sources out of `submodules/IntellijCommunity`
+via `maven-resources-plugin` and compiles them unchanged (only `internal` → public visibility is
+patched, so the NetBeans layer can construct them). `KaIdeaIntentionHint` / `KaIdeaInspectionHint`
+in `Nbm` drive them through reflection, reusing the `KaModCommandFix` PSI→document sync, and
+`KaStructuralConversionHints` attaches each action to the PSI element the caret refers to.
+
+| Conversion | IDEA K2 source | NetBeans adapter | Test |
+|---|---|---|---|
+| `a.plus(b)` → `a + b` | `code-insight/inspections-k2/.../expressions/ReplaceCallWithBinaryOperatorInspection.kt` | `KaIdeaInspectionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` |
+| `a.get(i)` / `a.set(i, v)` → `a[i]` / `a[i] = v` | `code-insight/inspections-k2/.../ReplaceGetOrSetInspection.kt` | `KaIdeaInspectionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` |
+| `a.contains(b)` → `b in a` | `code-insight/inspections-k2/.../ReplaceContainsInspection.kt` | `KaIdeaInspectionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` |
+| `a.invoke(b)` → `a(b)` | `code-insight/intentions-k2/.../ReplaceInvokeIntention.kt` | `KaIdeaIntentionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` |
+| `a.not()` / `a.unaryPlus()` → `!a` / `+a` | `code-insight/intentions-k2/.../ReplaceCallWithUnaryOperatorIntention.kt` | `KaIdeaIntentionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` |
+| `f() {}` → `f {}` | `code-insight/inspections-shared/.../RemoveEmptyParenthesesFromLambdaCallInspection.kt` | `KaIdeaInspectionHint` via `KaStructuralConversionHints` | `KaIntentionsTest` |
+
+The reverse directions (`a + b` → `a.plus(b)` as `KaOperatorToFunctionIntention`, and
+lambda → callable reference as `KaConvertLambdaToReferenceIntention`) were already hand-written
+before F8 and are **kept**: the IDEA counterparts (`OperatorToFunctionIntention`,
+`ConvertLambdaToReferenceIntention`) pull in a much larger slice of `code-insight` than these six
+conversions need, so replacing them is out of F8 scope.
## Representative compatibility baseline
@@ -111,5 +141,6 @@ The existing NetBeans tests are the initial executable sample set. They intentio
| Push Members Down into direct subclasses | `KaPushMembersDownComputerTest` | Build-wide Kotlin hierarchy baseline; advanced conflict scan remains incomplete |
| Standalone K2 override/super-method bridge | `KotlinStandaloneHierarchySearchTest` | Build-wide Kotlin hierarchy baseline; Java and full IDEA-index coverage remain incomplete |
| Introduce Type Alias on generic type text | `KaIntroduceTypeAliasTest` | Documented deviation: no generic type-parameter extraction |
+| Operator/function structural conversions driven from a caret hint | `KaIntentionsTest` | Exact parity baseline for the six IDEA K2 conversions; the reverse `a + b` → `a.plus(b)` and lambda → reference directions remain hand-written |
When a future port changes a row from Partial to Complete, it must add or strengthen the relevant compatibility fixture before changing this matrix status.
diff --git a/pom.xml b/pom.xml
index 10c838a2..159d8691 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.282-SNAPSHOT
+ 0.11.284-SNAPSHOT
pom
Netbeans Plugin Kotlin
@@ -36,6 +36,7 @@
KotlinFixesImpl
KotlinIcons
KotlinRefactoring
+ KotlinCodeInsightImpl
Nbm
@@ -221,6 +222,11 @@
netbeans-plugin-kotlin-refactoring
${project.version}
+
+ io.github.nbplugins
+ netbeans-plugin-kotlin-code-insight-impl
+ ${project.version}
+