diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19e0ab17..4b356197 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Added Inline Anonymous Function/Lambda refactoring: place the caret in an immediately invoked Kotlin lambda or anonymous function, press **Ctrl+Alt+N** or choose **Refactor → Inline...**, preview the one-call rewrite, and apply the IntelliJ IDEA K2-generated substitution; **Undo Last Refactoring** restores the original document.
- Added separate Java and Kotlin package trees in Project Explorer: conventional `src/main/java` and `src/test/java` remain under **Source Packages** and **Test Packages**, while `src/main/kotlin` and `src/test/kotlin` appear under **Kotlin Source Packages** and **Kotlin Test Packages** in Maven, Gradle, and Ant/J2SE projects.
- Added Inline Type Alias refactoring: place the caret on a Kotlin `typealias` declaration or one of its type uses, press **Ctrl+Alt+N** or choose **Refactor → Inline...**, then expand every supported Kotlin occurrence or only the selected use and optionally retain the alias declaration; generic arguments, imports, and parenthesisation use the IntelliJ IDEA K2 engine, while **Undo Last Refactoring** restores all changed Kotlin files.
- Added Change Kotlin Package refactoring: use **Refactor → Change Kotlin Package...** for the active Kotlin file or a folder's recursive Kotlin descendants to update package directives in place; nested files retain their relative package hierarchy, supported Kotlin code references and direct imports update, and **Undo Last Refactoring** restores the original contents.
diff --git a/CoreImpl/pom.xml b/CoreImpl/pom.xml
index 5324e6f1..76ef7293 100644
--- a/CoreImpl/pom.xml
+++ b/CoreImpl/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-core-impl
diff --git a/KotlinCompilerCliBase/pom.xml b/KotlinCompilerCliBase/pom.xml
index 9649602d..3020573a 100644
--- a/KotlinCompilerCliBase/pom.xml
+++ b/KotlinCompilerCliBase/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-compiler-cli-base
diff --git a/KotlinFixesImpl/pom.xml b/KotlinFixesImpl/pom.xml
index 3c55eb87..a12bf71b 100644
--- a/KotlinFixesImpl/pom.xml
+++ b/KotlinFixesImpl/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-fixes-impl
diff --git a/KotlinFormatter/pom.xml b/KotlinFormatter/pom.xml
index d4d7e20b..15535125 100644
--- a/KotlinFormatter/pom.xml
+++ b/KotlinFormatter/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-formatter
diff --git a/KotlinHighlighting/pom.xml b/KotlinHighlighting/pom.xml
index b98bb91b..8c6b9d9c 100644
--- a/KotlinHighlighting/pom.xml
+++ b/KotlinHighlighting/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-highlighting
diff --git a/KotlinIcons/pom.xml b/KotlinIcons/pom.xml
index 4bcefb0b..62ced0b0 100644
--- a/KotlinIcons/pom.xml
+++ b/KotlinIcons/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-icons
diff --git a/KotlinIntentionUtils/pom.xml b/KotlinIntentionUtils/pom.xml
index d21133ee..372d3085 100644
--- a/KotlinIntentionUtils/pom.xml
+++ b/KotlinIntentionUtils/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-intention-utils
diff --git a/KotlinRefactoring/pom.xml b/KotlinRefactoring/pom.xml
index e8004ee8..340fdc1b 100644
--- a/KotlinRefactoring/pom.xml
+++ b/KotlinRefactoring/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-refactoring
diff --git a/KotlinRefactoring/src/main/kotlin/io/github/nbplugins/kotlin/refactoring/KaInlineAnonymousFunctionComputer.kt b/KotlinRefactoring/src/main/kotlin/io/github/nbplugins/kotlin/refactoring/KaInlineAnonymousFunctionComputer.kt
new file mode 100644
index 00000000..fd2e1542
--- /dev/null
+++ b/KotlinRefactoring/src/main/kotlin/io/github/nbplugins/kotlin/refactoring/KaInlineAnonymousFunctionComputer.kt
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright 2000-2024 JetBrains s.r.o.
+ * 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.refactoring
+
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.kotlin.idea.k2.refactoring.inline.KotlinInlineAnonymousFunctionProcessor
+import org.jetbrains.kotlin.psi.KtExpression
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.KtFunction
+import org.jetbrains.kotlin.psi.KtFunctionLiteral
+import org.jetbrains.kotlin.psi.KtNamedFunction
+
+/**
+ * Prepared IDEA-engine data for a Kotlin **Inline Anonymous Function** refactoring.
+ *
+ * @param function lambda or anonymous function selected at the invocation caret
+ * @param call immediate invocation accepted by IDEA's anonymous-function processor
+ */
+data class KaInlineAnonymousFunctionResult(
+ val function: KtFunction,
+ val call: KtExpression,
+)
+
+/**
+ * Prepares an immediately invoked lambda or anonymous function for IDEA's K2 inline processor.
+ *
+ * IDEA remains responsible for recognising supported invocation shapes through
+ * [KotlinInlineAnonymousFunctionProcessor.findCallExpression]. The NetBeans adapter owns UI,
+ * preview, document persistence, and undo.
+ *
+ * @param cursorKtFile session-managed Kotlin file containing the invocation caret
+ * @param offset caret offset within [cursorKtFile]
+ */
+class KaInlineAnonymousFunctionComputer(
+ private val cursorKtFile: KtFile,
+ private val offset: Int,
+) {
+
+ /** Result of [compute]. */
+ sealed class Outcome {
+ /** The lambda/anonymous function and its IDEA-supported immediate call are ready. */
+ data class Ready(val result: KaInlineAnonymousFunctionResult) : Outcome()
+
+ /** A lambda/anonymous function was selected but is not immediately invoked. */
+ data class Error(val message: String) : Outcome()
+
+ /** The caret does not select a lambda or anonymous function. */
+ data object NotApplicable : Outcome()
+ }
+
+ /**
+ * Finds the lambda or anonymous function enclosing the caret and delegates call validation to IDEA.
+ *
+ * @return [Outcome.Ready] for an immediately invoked function, [Outcome.Error] for an uninvoked
+ * lambda/anonymous function, or [Outcome.NotApplicable] for all other caret targets
+ */
+ fun compute(): Outcome {
+ val element = cursorKtFile.findElementAt(offset) ?: return Outcome.NotApplicable
+ val function = PsiTreeUtil.getParentOfType(element, KtFunction::class.java, false)
+ ?.takeIf(::isAnonymousFunction)
+ ?: return Outcome.NotApplicable
+ val call = KotlinInlineAnonymousFunctionProcessor.findCallExpression(function)
+ ?: return Outcome.Error(messageFor(function))
+ return Outcome.Ready(KaInlineAnonymousFunctionResult(function, call))
+ }
+
+ /** Restricts this route to function literals and unnamed `fun` expressions, not named declarations. */
+ private fun isAnonymousFunction(function: KtFunction): Boolean =
+ function is KtFunctionLiteral || function is KtNamedFunction && function.nameIdentifier == null
+
+ /** Explains the IDEA eligibility restriction without importing IDEA's modal error UI. */
+ private fun messageFor(function: KtFunction): String =
+ if (function is KtFunctionLiteral) {
+ "Cannot inline this lambda expression because it is not immediately invoked."
+ } else {
+ "Cannot inline this anonymous function because it is not immediately invoked."
+ }
+}
diff --git a/Nbm/pom.xml b/Nbm/pom.xml
index e67bdd0d..6cfc81a4 100644
--- a/Nbm/pom.xml
+++ b/Nbm/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-nbm
diff --git a/Nbm/src/main/java/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionUI.java b/Nbm/src/main/java/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionUI.java
new file mode 100644
index 00000000..79dfece2
--- /dev/null
+++ b/Nbm/src/main/java/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionUI.java
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * 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.refactoring;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.refactoring.api.AbstractRefactoring;
+import org.netbeans.modules.refactoring.api.Problem;
+import org.netbeans.modules.refactoring.spi.ui.CustomRefactoringPanel;
+import org.netbeans.modules.refactoring.spi.ui.RefactoringUI;
+import org.openide.util.HelpCtx;
+
+/**
+ * NetBeans UI adapter for Kotlin **Inline Anonymous Function**.
+ *
+ * The transformation is limited to the immediately invoked lambda or anonymous function selected
+ * at the caret. IDEA's K2 processor supplies the actual rewrite; NetBeans supplies its preview
+ * and confirmation flow.
+ *
+ * @param refactoring carrier routed to {@link KotlinInlineAnonymousFunctionPlugin}
+ */
+public final class KotlinInlineAnonymousFunctionUI implements RefactoringUI {
+
+ private final KotlinInlineAnonymousFunctionRefactoring refactoring;
+
+ /**
+ * Creates an Inline Anonymous Function UI.
+ *
+ * @param refactoring carrier receiving the active document and caret
+ */
+ public KotlinInlineAnonymousFunctionUI(KotlinInlineAnonymousFunctionRefactoring refactoring) {
+ this.refactoring = refactoring;
+ }
+
+ @Override
+ public String getName() {
+ return "Inline Anonymous Function";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Inline immediately invoked lambda or anonymous function";
+ }
+
+ @Override
+ public boolean isQuery() {
+ return false;
+ }
+
+ @Override
+ public CustomRefactoringPanel getPanel(ChangeListener parent) {
+ return new DescriptionPanel(getDescription());
+ }
+
+ @Override
+ public Problem setParameters() {
+ return null;
+ }
+
+ @Override
+ public Problem checkParameters() {
+ return null;
+ }
+
+ @Override
+ public boolean hasParameters() {
+ return true;
+ }
+
+ @Override
+ public AbstractRefactoring getRefactoring() {
+ return refactoring;
+ }
+
+ @Override
+ public HelpCtx getHelpCtx() {
+ return null;
+ }
+
+ /** Displays the operation description until the user previews or confirms the refactoring. */
+ private static final class DescriptionPanel implements CustomRefactoringPanel {
+ private final JPanel component;
+
+ DescriptionPanel(String description) {
+ JLabel label = new JLabel(description);
+ label.setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12));
+ component = new JPanel(new BorderLayout());
+ component.add(label, BorderLayout.CENTER);
+ }
+
+ @Override public void initialize() {}
+ @Override public Component getComponent() { return component; }
+ }
+}
diff --git a/Nbm/src/main/java/org/jetbrains/kotlin/refactorings/rename/KotlinRefactoringsFactory.java b/Nbm/src/main/java/org/jetbrains/kotlin/refactorings/rename/KotlinRefactoringsFactory.java
index e9400f3c..015ee5de 100644
--- a/Nbm/src/main/java/org/jetbrains/kotlin/refactorings/rename/KotlinRefactoringsFactory.java
+++ b/Nbm/src/main/java/org/jetbrains/kotlin/refactorings/rename/KotlinRefactoringsFactory.java
@@ -34,6 +34,8 @@
import io.github.nbplugins.kotlin.nbm.refactoring.KotlinInlineVariableRefactoring;
import io.github.nbplugins.kotlin.nbm.refactoring.KotlinInlineTypeAliasPlugin;
import io.github.nbplugins.kotlin.nbm.refactoring.KotlinInlineTypeAliasRefactoring;
+import io.github.nbplugins.kotlin.nbm.refactoring.KotlinInlineAnonymousFunctionPlugin;
+import io.github.nbplugins.kotlin.nbm.refactoring.KotlinInlineAnonymousFunctionRefactoring;
import io.github.nbplugins.kotlin.nbm.refactoring.KotlinIntroduceConstantPlugin;
import io.github.nbplugins.kotlin.nbm.refactoring.KotlinIntroduceConstantRefactoring;
import io.github.nbplugins.kotlin.nbm.refactoring.KotlinCopyDeclarationPlugin;
@@ -86,6 +88,7 @@
*
{@link KotlinInlineVariableRefactoring} — delegates to {@link KotlinInlineVariablePlugin}
* {@link KotlinInlineFunctionRefactoring} — delegates to {@link KotlinInlineFunctionPlugin}
* {@link KotlinInlineTypeAliasRefactoring} — delegates to {@link KotlinInlineTypeAliasPlugin}
+ * {@link KotlinInlineAnonymousFunctionRefactoring} — delegates to {@link KotlinInlineAnonymousFunctionPlugin}
* {@link KotlinIntroduceVariableRefactoring} — delegates to {@link KotlinIntroduceVariablePlugin}
* {@link KotlinExtractFunctionRefactoring} — delegates to {@link KotlinExtractFunctionPlugin}
* {@link KotlinIntroduceConstantRefactoring} — delegates to {@link KotlinIntroduceConstantPlugin}
@@ -138,6 +141,9 @@ public RefactoringPlugin createInstance(AbstractRefactoring refactoring) {
if (refactoring instanceof KotlinInlineTypeAliasRefactoring) {
return new KotlinInlineTypeAliasPlugin((KotlinInlineTypeAliasRefactoring) refactoring);
}
+ if (refactoring instanceof KotlinInlineAnonymousFunctionRefactoring) {
+ return new KotlinInlineAnonymousFunctionPlugin((KotlinInlineAnonymousFunctionRefactoring) refactoring);
+ }
if (refactoring instanceof KotlinIntroduceVariableRefactoring) {
return new KotlinIntroduceVariablePlugin((KotlinIntroduceVariableRefactoring) refactoring);
}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionPlugin.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionPlugin.kt
new file mode 100644
index 00000000..aad8a131
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionPlugin.kt
@@ -0,0 +1,252 @@
+/*******************************************************************************
+ * 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.refactoring
+
+import io.github.nbplugins.kotlin.nbm.navigation.KotlinFindUsagesResultElement
+import io.github.nbplugins.kotlin.nbm.resolve.KotlinAnalysisAPISession
+import io.github.nbplugins.kotlin.refactoring.KaInlineAnonymousFunctionComputer
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.kotlin.idea.k2.refactoring.inline.KotlinInlineAnonymousFunctionProcessor
+import org.jetbrains.kotlin.idea.k2.refactoring.util.LambdaToAnonymousFunctionUtil
+import org.jetbrains.kotlin.log.KotlinLogger
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.KtFunctionLiteral
+import org.jetbrains.kotlin.psi.KtNamedFunction
+import org.jetbrains.kotlin.psi.KtSimpleNameExpression
+import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
+import org.jetbrains.kotlin.utils.ProjectUtils
+import org.netbeans.modules.csl.api.OffsetRange
+import org.netbeans.modules.refactoring.api.Problem
+import org.netbeans.modules.refactoring.spi.ProgressProviderAdapter
+import org.netbeans.modules.refactoring.spi.RefactoringElementsBag
+import org.netbeans.modules.refactoring.spi.RefactoringPlugin
+import org.netbeans.modules.refactoring.spi.SimpleRefactoringElementImplementation
+import org.openide.filesystems.FileObject
+import org.openide.filesystems.FileUtil
+import org.openide.loaders.DataObject
+import org.openide.text.CloneableEditorSupport
+import org.openide.text.PositionBounds
+import org.openide.util.Lookup
+import org.openide.util.lookup.Lookups
+import javax.swing.text.Position.Bias
+import javax.swing.text.StyledDocument
+
+/**
+ * Bridges NetBeans Inline Anonymous Function UI to IDEA's K2 inline processor.
+ *
+ * @param refactoring active document and caret selected by the user
+ */
+class KotlinInlineAnonymousFunctionPlugin(
+ private val refactoring: KotlinInlineAnonymousFunctionRefactoring,
+) : ProgressProviderAdapter(), RefactoringPlugin {
+
+ override fun preCheck(): Problem? = null
+ override fun fastCheckParameters(): Problem? = null
+ override fun checkParameters(): Problem? = null
+ override fun cancelRequest() {}
+
+ /**
+ * Validates the immediate invocation and exposes it as the preview item plus one apply element.
+ *
+ * @param bag NetBeans refactoring element bag to populate
+ * @return fatal validation problem, or `null` when a valid plan was prepared
+ */
+ override fun prepare(bag: RefactoringElementsBag): Problem? {
+ val sourceFile = ProjectUtils.getFileObjectForDocument(refactoring.doc) ?: return null
+ val nbProject = ProjectUtils.getKotlinProjectForFileObject(sourceFile) ?: return null
+ val session = KotlinAnalysisAPISession.getSession(nbProject)
+ val ktFile = session.getKtFileForPath(sourceFile.path) ?: return null
+ return when (val outcome = KaInlineAnonymousFunctionComputer(ktFile, refactoring.offset).compute()) {
+ is KaInlineAnonymousFunctionComputer.Outcome.NotApplicable -> null
+ is KaInlineAnonymousFunctionComputer.Outcome.Error -> Problem(true, outcome.message)
+ is KaInlineAnonymousFunctionComputer.Outcome.Ready -> {
+ val range = outcome.result.call.textRange
+ bag.add(
+ refactoring,
+ KotlinFindUsagesResultElement(OffsetRange(range.startOffset, range.endOffset), sourceFile),
+ )
+ bag.add(
+ refactoring,
+ KotlinInlineAnonymousFunctionApplyElement(sourceFile, nbProject, sourceFile.path, refactoring.offset),
+ )
+ null
+ }
+ }
+ }
+}
+
+/**
+ * Applies one IDEA-generated anonymous-function inline rewrite as a hunk-preserving transaction.
+ *
+ * @param sourceFile parent source file used by NetBeans preview and undo infrastructure
+ * @param nbProject project owning the standalone K2 session
+ * @param cursorFilePath session virtual path of [sourceFile]
+ * @param cursorOffset invocation caret offset within [cursorFilePath]
+ */
+class KotlinInlineAnonymousFunctionApplyElement(
+ private val sourceFile: FileObject,
+ private val nbProject: org.netbeans.api.project.Project,
+ private val cursorFilePath: String,
+ private val cursorOffset: Int,
+) : SimpleRefactoringElementImplementation() {
+
+ private var transaction: KotlinRefactoringTransaction? = null
+
+ override fun getText(): String = "Inline anonymous function"
+ override fun getDisplayText(): String = text
+ override fun getLookup(): Lookup = Lookups.fixed(sourceFile)
+ override fun getParentFile(): FileObject = sourceFile
+
+ override fun getPosition(): PositionBounds? = try {
+ val support = DataObject.find(sourceFile).lookup.lookup(CloneableEditorSupport::class.java) ?: return null
+ PositionBounds(support.createPositionRef(0, Bias.Forward), support.createPositionRef(0, Bias.Backward))
+ } catch (_: Exception) {
+ null
+ }
+
+ /**
+ * Rebuilds the K2 session after IDEA converts a lambda to an anonymous function, then delegates
+ * the actual call substitution to IDEA's existing named-function inline strategy.
+ *
+ * The upstream anonymous-function processor performs both phases against one mutable IDEA
+ * session. Standalone K2 cannot attach FIR to the newly inserted anonymous `fun` in that same
+ * session, so this adapter persists neither intermediate form nor partial mutation: it derives
+ * the intermediate text in PSI, refreshes the standalone session, and commits only final text.
+ */
+ override fun performChange() {
+ var pending: KotlinRefactoringTransaction? = null
+ try {
+ val firstSession = KotlinAnalysisAPISession.getSession(nbProject)
+ val cursorFile = firstSession.getKtFileForPath(cursorFilePath) ?: return
+ val firstReady = KaInlineAnonymousFunctionComputer(cursorFile, cursorOffset).compute()
+ as? KaInlineAnonymousFunctionComputer.Outcome.Ready ?: return
+ val originalSourceText = cursorFile.text
+ val affectedFile = fileObjectFor(firstReady.result.call.containingKtFile) ?: return
+ val currentTransaction = KotlinRefactoringTransaction()
+ pending = currentTransaction
+ currentTransaction.captureExisting(affectedFile)
+ val functionOffset = firstReady.result.function.textRange.startOffset
+ val inlineFile: KtFile
+ val namedFunction: KtNamedFunction
+ if (firstReady.result.function is KtFunctionLiteral) {
+ val convertedText = convertLambdaToAnonymousFunction(cursorFile, firstReady.result.function)
+ KotlinAnalysisAPISession.invalidate(nbProject)
+ val refreshedSession = KotlinAnalysisAPISession.getSession(nbProject)
+ // A refreshed session initially reads the unchanged on-disk source. Install the
+ // intermediate PSI text in its in-memory file before resolving it, so K2 builds FIR
+ // for the converted anonymous `fun` without ever saving that intermediate state.
+ refreshedSession.updateFileContent(cursorFilePath, convertedText)
+ inlineFile = refreshedSession.getKtFileForPath(cursorFilePath)
+ ?: error("Inline Anonymous Function could not refresh the source file.")
+ namedFunction = PsiTreeUtil.getParentOfType(
+ inlineFile.findElementAt(functionOffset),
+ KtNamedFunction::class.java,
+ false,
+ )?.takeIf { it.nameIdentifier == null }
+ ?: error("Inline Anonymous Function could not resolve the converted lambda.")
+ } else {
+ inlineFile = cursorFile
+ namedFunction = firstReady.result.function as? KtNamedFunction
+ ?: error("Inline Anonymous Function expected an anonymous function.")
+ }
+ val codeToInline = org.jetbrains.kotlin.idea.k2.refactoring.inline
+ .createCodeToInlineForFunction(namedFunction, editor = null, fallbackToSuperCall = false)
+ ?: error("Inline Anonymous Function could not prepare inline code.")
+ val call = KotlinInlineAnonymousFunctionProcessor.findCallExpression(namedFunction)
+ ?: error("Inline Anonymous Function could not resolve the immediate invocation.")
+ remapAnonymousParameters(codeToInline, namedFunction, call)
+ org.jetbrains.kotlin.idea.k2.refactoring.inline.codeInliner.CodeInliner(
+ usageExpression = null,
+ call = call,
+ inlineSetter = false,
+ replacement = codeToInline,
+ ).doInline() ?: error("Inline Anonymous Function did not produce a replacement.")
+
+ check(inlineFile.text != originalSourceText) {
+ "Inline Anonymous Function did not change the source text."
+ }
+ currentTransaction.stageHunkText(affectedFile, inlineFile.text, nbProject)
+ currentTransaction.commit()
+ transaction = currentTransaction
+ pending = null
+ KotlinAnalysisAPISession.invalidate(nbProject)
+ } catch (error: Throwable) {
+ KotlinLogger.INSTANCE.logException("KotlinInlineAnonymousFunctionApplyElement.performChange failed", error)
+ runCatching { pending?.rollback() }
+ .onFailure {
+ KotlinLogger.INSTANCE.logException(
+ "KotlinInlineAnonymousFunctionApplyElement rollback failed",
+ it,
+ )
+ }
+ KotlinAnalysisAPISession.invalidate(nbProject)
+ }
+ }
+
+ /**
+ * Replaces IDEA's anonymous-function synthetic parameter keys (`p1`, `p2`, ...) with the real
+ * names exposed by K2's invoke-call argument mapping.
+ *
+ * IDEA normally reaches this path through an internal mutable processor session whose PSI keeps
+ * synthetic parameter identities. The standalone session refresh deliberately rebuilds PSI, so
+ * the shared inliner sees source parameter names instead. Rebinding the copied inline template
+ * retains the upstream argument-mapping engine without manually substituting source text.
+ */
+ private fun remapAnonymousParameters(
+ codeToInline: org.jetbrains.kotlin.idea.refactoring.inline.codeInliner.CodeToInline,
+ function: KtNamedFunction,
+ call: org.jetbrains.kotlin.psi.KtExpression,
+ ) {
+ val arguments = (call as? org.jetbrains.kotlin.psi.KtCallExpression)
+ ?.valueArguments
+ ?.mapNotNull { it.getArgumentExpression() }
+ ?: return
+ function.valueParameters.forEachIndexed { index, parameter ->
+ val argument = arguments.getOrNull(index) ?: return@forEachIndexed
+ codeToInline.mainExpression
+ ?.collectDescendantsOfType()
+ ?.filter { it.text == parameter.name }
+ ?.forEach { reference -> reference.replace(argument.copy()) }
+ }
+ }
+
+ /** Returns intermediate source where a lambda has IDEA's K2-derived anonymous-function signature. */
+ private fun convertLambdaToAnonymousFunction(cursorFile: KtFile, function: org.jetbrains.kotlin.psi.KtFunction): String {
+ val lambda = (function as? KtFunctionLiteral)?.parent as? org.jetbrains.kotlin.psi.KtLambdaExpression
+ ?: return cursorFile.text
+ val functionText = LambdaToAnonymousFunctionUtil.prepareFunctionText(lambda)
+ ?: return cursorFile.text
+ LambdaToAnonymousFunctionUtil.convertLambdaToFunction(lambda, functionText)
+ return cursorFile.text
+ }
+
+ /** Restores the exact pre-refactoring source snapshot. */
+ override fun undoChange() {
+ runCatching { transaction?.undo() }
+ .onFailure {
+ KotlinLogger.INSTANCE.logException(
+ "KotlinInlineAnonymousFunctionApplyElement.undoChange failed",
+ it,
+ )
+ }
+ KotlinAnalysisAPISession.invalidate(nbProject)
+ }
+
+ /** Converts a session virtual path to its writable NetBeans file. */
+ private fun fileObjectFor(file: KtFile): FileObject? =
+ file.virtualFile?.path?.let { FileUtil.toFileObject(FileUtil.normalizeFile(java.io.File(it))) }
+}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionRefactoring.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionRefactoring.kt
new file mode 100644
index 00000000..75e8f290
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineAnonymousFunctionRefactoring.kt
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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.refactoring
+
+import org.netbeans.modules.refactoring.api.AbstractRefactoring
+import org.openide.util.lookup.Lookups
+import javax.swing.text.StyledDocument
+
+/**
+ * Carries the active document and caret through NetBeans Inline Anonymous Function UI.
+ *
+ * @param doc Kotlin editor document containing the immediately invoked lambda or anonymous function
+ * @param offset caret offset within [doc]
+ */
+class KotlinInlineAnonymousFunctionRefactoring(
+ val doc: StyledDocument,
+ val offset: Int,
+) : AbstractRefactoring(Lookups.fixed(doc, Integer.valueOf(offset)))
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineVariableAction.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineVariableAction.kt
index 4f216387..097f6a77 100644
--- a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineVariableAction.kt
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineVariableAction.kt
@@ -22,10 +22,13 @@ import io.github.nbplugins.kotlin.nbm.resolve.KotlinAnalysisAPISession
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.log.KotlinLogger
+import org.jetbrains.kotlin.psi.KtFunction
+import org.jetbrains.kotlin.psi.KtFunctionLiteral
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtTypeAlias
+import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import com.intellij.psi.PsiElement
@@ -38,13 +41,15 @@ import javax.swing.text.JTextComponent
import javax.swing.text.StyledDocument
/**
- * Editor action for **Ctrl+Alt+N** — "Inline" for Kotlin functions, properties, and type aliases.
+ * Editor action for **Ctrl+Alt+N** — "Inline" for Kotlin functions, properties, type aliases, and immediate lambdas.
*
* Registered under action name [ACTION_NAME] in `layer.xml` for `text/x-kotlin`.
* Dispatches to the function or variable inline UI based on what is under the cursor:
* - [KtNamedFunction] → [KotlinInlineFunctionUI] + [KotlinInlineFunctionRefactoring]
* - [KtProperty] → [KotlinInlineVariableUI] + [KotlinInlineVariableRefactoring]
* - [KtTypeAlias] → [KotlinInlineTypeAliasUI] + [KotlinInlineTypeAliasRefactoring]
+ * - [KtFunctionLiteral] / unnamed [KtNamedFunction] → [KotlinInlineAnonymousFunctionUI] +
+ * [KotlinInlineAnonymousFunctionRefactoring]
*
* The action does **no validation** on its own — the plugin's `prepare()` validates and surfaces
* a fatal `Problem` if the symbol cannot be inlined. This keeps the action thin and lets the
@@ -70,10 +75,22 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR
runCatching {
when (val targetDeclaration = resolveTargetAt(doc, selectionStart, selectionEnd)) {
- is KtNamedFunction -> UI.openRefactoringUI(
- KotlinInlineFunctionUI(targetDeclaration.name.orEmpty(), KotlinInlineFunctionRefactoring(doc, targetOffset)),
- TopComponent.getRegistry().activated,
- )
+ is KtNamedFunction -> if (inlineTargetKind(targetDeclaration) == InlineTargetKind.ANONYMOUS_FUNCTION) {
+ UI.openRefactoringUI(
+ KotlinInlineAnonymousFunctionUI(
+ KotlinInlineAnonymousFunctionRefactoring(doc, targetOffset),
+ ),
+ TopComponent.getRegistry().activated,
+ )
+ } else {
+ UI.openRefactoringUI(
+ KotlinInlineFunctionUI(
+ targetDeclaration.name.orEmpty(),
+ KotlinInlineFunctionRefactoring(doc, targetOffset),
+ ),
+ TopComponent.getRegistry().activated,
+ )
+ }
is KtProperty -> UI.openRefactoringUI(
KotlinInlineVariableUI(targetDeclaration.name.orEmpty(), KotlinInlineVariableRefactoring(doc, targetOffset)),
TopComponent.getRegistry().activated,
@@ -82,6 +99,10 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR
KotlinInlineTypeAliasUI(targetDeclaration.name.orEmpty(), KotlinInlineTypeAliasRefactoring(doc, targetOffset)),
TopComponent.getRegistry().activated,
)
+ is KtFunction -> UI.openRefactoringUI(
+ KotlinInlineAnonymousFunctionUI(KotlinInlineAnonymousFunctionRefactoring(doc, targetOffset)),
+ TopComponent.getRegistry().activated,
+ )
null -> return@runCatching
}
}.onFailure { e ->
@@ -105,7 +126,7 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR
doc: StyledDocument,
selectionStart: Int,
selectionEnd: Int,
- ): KtNamedDeclaration? {
+ ): KtDeclaration? {
val fo = ProjectUtils.getFileObjectForDocument(doc) ?: return null
val project = ProjectUtils.getKotlinProjectForFileObject(fo)
?: ProjectUtils.getValidProject()
@@ -118,11 +139,11 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR
}
/** Resolves a direct caret target from a session-managed Kotlin file. */
- internal fun resolveTargetAt(ktFile: KtFile, offset: Int): KtNamedDeclaration? =
+ internal fun resolveTargetAt(ktFile: KtFile, offset: Int): KtDeclaration? =
resolveTargetAt(ktFile, offset, offset)
/**
- * Resolves the property/function/type alias covered by a caret or selection in a session-managed Kotlin file.
+ * Resolves the property, named function, type alias, lambda, or anonymous function covered by a caret or selection.
*
* @param selectionStart first selected offset, or the caret offset for an empty selection
* @param selectionEnd offset immediately after the selection, or [selectionStart] when empty
@@ -132,7 +153,7 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR
ktFile: KtFile,
selectionStart: Int,
selectionEnd: Int,
- ): KtNamedDeclaration? {
+ ): KtDeclaration? {
val offsets = if (selectionStart < selectionEnd) {
intArrayOf(selectionStart, selectionEnd - 1)
} else {
@@ -143,11 +164,32 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR
resolveReferenceTarget(element)?.let { return it }
((element.parent as? KtProperty)
?: (element.parent as? KtNamedFunction)
- ?: (element.parent as? KtTypeAlias))?.let { return it }
+ ?: (element.parent as? KtTypeAlias)
+ ?: PsiTreeUtil.getParentOfType(element, KtFunctionLiteral::class.java, false)
+ ?: PsiTreeUtil.getParentOfType(element, KtNamedFunction::class.java, false)
+ ?.takeIf { it.nameIdentifier == null })?.let { return it }
}
return null
}
+ /**
+ * Distinguishes a declaration from an anonymous `fun` expression before selecting an Inline UI.
+ *
+ * @param function function PSI selected by the user
+ * @return the corresponding Inline action category
+ */
+ internal fun inlineTargetKind(function: KtNamedFunction): InlineTargetKind =
+ if (function.nameIdentifier == null) InlineTargetKind.ANONYMOUS_FUNCTION else InlineTargetKind.NAMED_FUNCTION
+
+ /** Categories of [KtNamedFunction] supported by the unified Inline action. */
+ internal enum class InlineTargetKind {
+ /** An unnamed `fun` expression requiring the anonymous-function adapter. */
+ ANONYMOUS_FUNCTION,
+
+ /** A named declaration requiring the regular Inline Function adapter. */
+ NAMED_FUNCTION,
+ }
+
/** Resolves a direct Kotlin reference and retains only declarations supported by Inline. */
private fun resolveReferenceTarget(element: PsiElement): KtNamedDeclaration? {
val reference = PsiTreeUtil.getParentOfType(element, KtNameReferenceExpression::class.java, false)
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KaInlineAnonymousFunctionTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KaInlineAnonymousFunctionTest.kt
new file mode 100644
index 00000000..d72f3dcd
--- /dev/null
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KaInlineAnonymousFunctionTest.kt
@@ -0,0 +1,249 @@
+/*******************************************************************************
+ * 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.refactoring
+
+import io.github.nbplugins.kotlin.nbm.resolve.KotlinAnalysisAPISession
+import io.github.nbplugins.kotlin.refactoring.KaInlineAnonymousFunctionComputer
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.kotlin.idea.k2.refactoring.inline.KotlinInlineAnonymousFunctionProcessor
+import org.jetbrains.kotlin.idea.k2.refactoring.inline.codeInliner.CodeInliner
+import org.jetbrains.kotlin.idea.k2.refactoring.inline.createCodeToInlineForFunction
+import org.jetbrains.kotlin.idea.k2.refactoring.util.LambdaToAnonymousFunctionUtil
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.KtFunctionLiteral
+import org.jetbrains.kotlin.psi.KtLambdaExpression
+import org.jetbrains.kotlin.psi.KtNamedFunction
+import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
+import org.jetbrains.kotlin.idea.references.mainReference
+import utils.KotlinTestCase
+import java.nio.file.Files
+import java.nio.file.Path
+
+/** Tests standalone preparation and IDEA K2 application of Inline Anonymous Function. */
+class KaInlineAnonymousFunctionTest : KotlinTestCase(
+ "KaInlineAnonymousFunctionTest",
+ "inlineAnonymousFunction",
+) {
+
+ private companion object {
+ const val CARET = ""
+ }
+
+ /** Finds the Kotlin standard library JAR required by an isolated standalone K2 session. */
+ private fun findKotlinStdlib(): Path? =
+ System.getProperty("java.class.path")
+ .split(System.getProperty("path.separator"))
+ .map { Path.of(it) }
+ .firstOrNull { it.fileName?.toString()?.startsWith("kotlin-stdlib") == true && it.toFile().exists() }
+
+ /** Creates a temporary standalone session for one source fixture. */
+ private fun prepare(fixture: String): Triple? {
+ val stdlib = findKotlinStdlib() ?: return null
+ val fixtureFolder = dir.getFileObject(fixture) ?: error("Missing fixture: $fixture")
+ val source = fixtureFolder.getFileObject("file.kt")?.asText() ?: error("Missing source for $fixture")
+ val marker = source.indexOf(CARET)
+ check(marker >= 0) { "Missing caret marker in $fixture" }
+ val temporaryDirectory = Files.createTempDirectory("nbkotlin-inline-anonymous-$fixture")
+ val temporaryFile = temporaryDirectory.resolve("file.kt")
+ Files.writeString(temporaryFile, source.replace(CARET, ""))
+ val session = KotlinAnalysisAPISession.createWithJars(
+ moduleName = "inline-anonymous-$fixture",
+ binaryJars = listOf(stdlib),
+ sourceRoots = listOf(temporaryDirectory),
+ )
+ val ktFile = session.getKtFileForPath(temporaryFile.toString()) ?: error("Missing KtFile for $temporaryFile")
+ return Triple(KaInlineAnonymousFunctionComputer(ktFile, marker), ktFile, temporaryDirectory)
+ }
+
+ /** An immediately invoked lambda is recognised through IDEA's call-shape detector. */
+ fun testLambdaCaret_preparesImmediateInvocation() {
+ val (computer, _, temporaryDirectory) = prepare("lambda") ?: return
+ try {
+ val outcome = computer.compute()
+ assertTrue("Expected a ready lambda inline plan, got $outcome", outcome is KaInlineAnonymousFunctionComputer.Outcome.Ready)
+ } finally {
+ temporaryDirectory.toFile().deleteRecursively()
+ }
+ }
+
+ /** An immediately invoked anonymous `fun` expression is recognised through IDEA's call-shape detector. */
+ fun testAnonymousFunctionCaret_preparesImmediateInvocation() {
+ val (computer, _, temporaryDirectory) = prepare("anonymous") ?: return
+ try {
+ val outcome = computer.compute()
+ assertTrue("Expected a ready anonymous-function inline plan, got $outcome", outcome is KaInlineAnonymousFunctionComputer.Outcome.Ready)
+ } finally {
+ temporaryDirectory.toFile().deleteRecursively()
+ }
+ }
+
+ /** A lambda stored for later use must be rejected instead of being rewritten unsafely. */
+ fun testNonInvokedLambda_isRejected() {
+ val (computer, _, temporaryDirectory) = prepare("notInvoked") ?: return
+ try {
+ val outcome = computer.compute()
+ assertTrue("Expected an eligibility error, got $outcome", outcome is KaInlineAnonymousFunctionComputer.Outcome.Error)
+ } finally {
+ temporaryDirectory.toFile().deleteRecursively()
+ }
+ }
+
+ /**
+ * The upstream processor links, while the NetBeans adapter performs its required K2-session
+ * refresh between lambda conversion and the shared IDEA code-inliner phase.
+ */
+ fun testIdeaProcessor_requiresSessionRefreshForLambda() {
+ val (computer, _, temporaryDirectory) = prepare("lambda") ?: return
+ try {
+ val ready = computer.compute() as? KaInlineAnonymousFunctionComputer.Outcome.Ready
+ assertNotNull("Expected a ready lambda inline plan", ready)
+ assertNotNull("The upstream processor must accept the immediate call", ready!!.result.call)
+ } finally {
+ temporaryDirectory.toFile().deleteRecursively()
+ }
+ }
+
+ /**
+ * Reproduces the adapter's complete refreshed-session inline path for a lambda with an argument.
+ *
+ * The assertion deliberately captures the production defect: the current direct `CodeInliner`
+ * invocation removes the call but does not substitute its argument for `value`.
+ */
+ fun testLambdaInline_substitutesInvocationArgument() {
+ val (computer, firstFile, temporaryDirectory) = prepare("lambda") ?: return
+ try {
+ val outcome = computer.compute()
+ assertTrue("Expected a ready lambda inline plan", outcome is KaInlineAnonymousFunctionComputer.Outcome.Ready)
+ val ready = outcome as KaInlineAnonymousFunctionComputer.Outcome.Ready
+ val functionOffset = ready.result.function.textRange.startOffset
+ val lambda = ((ready.result.function as? KtFunctionLiteral)?.parent as? KtLambdaExpression)
+ ?: error("Expected lambda expression")
+ val functionText = LambdaToAnonymousFunctionUtil.prepareFunctionText(lambda)
+ ?: error("Expected lambda signature")
+ LambdaToAnonymousFunctionUtil.convertLambdaToFunction(lambda, functionText)
+
+ val refreshed = KotlinAnalysisAPISession.createWithJars(
+ moduleName = "inline-anonymous-refreshed",
+ binaryJars = listOfNotNull(findKotlinStdlib()),
+ sourceRoots = listOf(temporaryDirectory),
+ )
+ val sourcePath = temporaryDirectory.resolve("file.kt").toString()
+ refreshed.updateFileContent(sourcePath, firstFile.text)
+ val refreshedFile: KtFile = refreshed.getKtFileForPath(sourcePath)
+ ?: error("Missing refreshed KtFile")
+ val namedFunction = PsiTreeUtil.getParentOfType(
+ refreshedFile.findElementAt(functionOffset),
+ KtNamedFunction::class.java,
+ false,
+ ) ?: error("Expected converted anonymous function")
+ val call = KotlinInlineAnonymousFunctionProcessor.findCallExpression(namedFunction)
+ ?: error("Expected immediate invocation")
+ val codeToInline = createCodeToInlineForFunction(
+ namedFunction,
+ editor = null,
+ fallbackToSuperCall = false,
+ ) ?: error("Expected inline code")
+
+ val argument = (call as org.jetbrains.kotlin.psi.KtCallExpression)
+ .valueArguments
+ .single()
+ .getArgumentExpression()
+ ?: error("Expected call argument")
+ codeToInline.mainExpression
+ ?.collectDescendantsOfType()
+ ?.filter { it.text == namedFunction.valueParameters.single().name }
+ ?.forEach { it.replace(argument.copy()) }
+
+ CodeInliner(
+ usageExpression = null,
+ call = call,
+ inlineSetter = false,
+ replacement = codeToInline,
+ ).doInline()
+
+ assertTrue(
+ "Inline result must substitute 41 for the lambda parameter, but was:\n${refreshedFile.text}",
+ "val answer = 41 + 1" in refreshedFile.text,
+ )
+ } finally {
+ temporaryDirectory.toFile().deleteRecursively()
+ }
+ }
+
+ /**
+ * Reproduces inline application for an already-written anonymous function without refreshing
+ * its valid standalone K2 session. Only lambda conversion creates new PSI requiring refresh.
+ */
+ fun testAnonymousFunctionInline_substitutesInvocationArgument() {
+ val (computer, firstFile, temporaryDirectory) = prepare("anonymous") ?: return
+ try {
+ val outcome = computer.compute()
+ assertTrue("Expected a ready anonymous-function inline plan", outcome is KaInlineAnonymousFunctionComputer.Outcome.Ready)
+ val ready = outcome as KaInlineAnonymousFunctionComputer.Outcome.Ready
+ val namedFunction = ready.result.function as? KtNamedFunction
+ ?: error("Expected anonymous function")
+ assertNull("Existing anonymous function must be unnamed", namedFunction.nameIdentifier)
+ val inlineFile = firstFile
+ val call = KotlinInlineAnonymousFunctionProcessor.findCallExpression(namedFunction)
+ ?: error("Expected immediate invocation")
+ val codeToInline = createCodeToInlineForFunction(
+ namedFunction,
+ editor = null,
+ fallbackToSuperCall = false,
+ ) ?: error("Expected inline code")
+
+ val argument = (call as org.jetbrains.kotlin.psi.KtCallExpression)
+ .valueArguments
+ .single()
+ .getArgumentExpression()
+ ?: error("Expected call argument")
+ codeToInline.mainExpression
+ ?.collectDescendantsOfType()
+ ?.filter { it.text == namedFunction.valueParameters.single().name }
+ ?.forEach { it.replace(argument.copy()) }
+
+ CodeInliner(
+ usageExpression = null,
+ call = call,
+ inlineSetter = false,
+ replacement = codeToInline,
+ ).doInline()
+
+ assertTrue(
+ "Inline result must substitute 41 for the anonymous-function parameter, but was:\n${inlineFile.text}",
+ "val answer = 41 + 1" in inlineFile.text,
+ )
+ } finally {
+ temporaryDirectory.toFile().deleteRecursively()
+ }
+ }
+
+ /** The upstream processor must link so NetBeans can retain IDEA's transformation semantics. */
+ fun testIdeaInlineAnonymousFunctionProcessor_isLinkable() {
+ try {
+ Class.forName(
+ "org.jetbrains.kotlin.idea.k2.refactoring.inline.KotlinInlineAnonymousFunctionProcessor",
+ true,
+ javaClass.classLoader,
+ )
+ } catch (error: NoClassDefFoundError) {
+ fail("KotlinInlineAnonymousFunctionProcessor cannot link: ${error.message}")
+ } catch (error: ClassNotFoundException) {
+ fail("KotlinInlineAnonymousFunctionProcessor is missing: ${error.message}")
+ }
+ }
+}
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineVariableActionTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineVariableActionTest.kt
index 566cec7e..41c231d9 100644
--- a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineVariableActionTest.kt
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineVariableActionTest.kt
@@ -18,6 +18,7 @@ package io.github.nbplugins.kotlin.nbm.refactoring
import io.github.nbplugins.kotlin.nbm.resolve.KotlinAnalysisAPISession
import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.KtFunctionLiteral
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtTypeAlias
@@ -156,6 +157,43 @@ class KotlinInlineVariableActionTest : NbTestCase("KotlinInlineVariableActionTes
}
}
+ /** An immediately invoked lambda is routed to the anonymous-function Inline adapter. */
+ fun testImmediatelyInvokedLambda_resolvesFunctionLiteral() {
+ val source = """
+ fun main() {
+ val answer = ({ value: Int -> value + 1 })(41)
+ println(answer)
+ }
+ """.trimIndent()
+ withKtFile(source) { ktFile ->
+ val offset = source.indexOf("value: Int")
+ val target = KotlinInlineVariableAction().resolveTargetAt(ktFile, offset)
+
+ assertTrue("Expected a lambda target, got $target", target is KtFunctionLiteral)
+ }
+ }
+
+ /** An unnamed function expression must be routed to the anonymous-function Inline adapter. */
+ fun testImmediatelyInvokedAnonymousFunction_routesToAnonymousInline() {
+ val source = """
+ fun main() {
+ val answer = (fun(value: Int): Int = value + 1)(41)
+ println(answer)
+ }
+ """.trimIndent()
+ withKtFile(source) { ktFile ->
+ val offset = source.indexOf("value: Int")
+ val target = KotlinInlineVariableAction().resolveTargetAt(ktFile, offset)
+
+ assertTrue("Expected an anonymous function target, got $target", target is KtNamedFunction)
+ assertNull("Expected an unnamed anonymous function", (target as KtNamedFunction).nameIdentifier)
+ assertEquals(
+ KotlinInlineVariableAction.InlineTargetKind.ANONYMOUS_FUNCTION,
+ KotlinInlineVariableAction().inlineTargetKind(target),
+ )
+ }
+ }
+
/** A type-alias declaration remains a valid unified Inline target. */
fun testTypeAliasDeclaration_resolvesTypeAlias() {
val source = """
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 75d1b796..7b00d3ac 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
@@ -54,10 +54,10 @@ class RefactoringCoverageBaselineTest : NbTestCase("RefactoringCoverageBaselineT
}
}
- val totals = "**Baseline counts:** 0 complete, 21 partial, 3 absent, 24 total."
+ val totals = "**Baseline counts:** 0 complete, 22 partial, 2 absent, 24 total."
assertTrue("Visible baseline totals must match the records", Files.readString(matrix).contains(totals))
- assertEquals(21, records.count { it.status == "partial" })
- assertEquals(3, records.count { it.status == "absent" })
+ assertEquals(22, records.count { it.status == "partial" })
+ assertEquals(2, records.count { it.status == "absent" })
}
/** Finds the checkout root from Surefire's module working directory. */
diff --git a/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/anonymous/file.kt b/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/anonymous/file.kt
new file mode 100644
index 00000000..0e310e3d
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/anonymous/file.kt
@@ -0,0 +1,4 @@
+fun main() {
+ val answer = (fun(value: Int): Int = value + 1)(41)
+ println(answer)
+}
diff --git a/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/lambda/file.kt b/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/lambda/file.kt
new file mode 100644
index 00000000..3e8e20d7
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/lambda/file.kt
@@ -0,0 +1,4 @@
+fun main() {
+ val answer = ({ value: Int -> value + 1 })(41)
+ println(answer)
+}
diff --git a/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/notInvoked/file.kt b/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/notInvoked/file.kt
new file mode 100644
index 00000000..3facf65b
--- /dev/null
+++ b/Nbm/src/test/resources/projForTest/src/inlineAnonymousFunction/notInvoked/file.kt
@@ -0,0 +1,4 @@
+fun main() {
+ val increment = { value: Int -> value + 1 }
+ println(increment(41))
+}
diff --git a/docs/development-plan.md b/docs/development-plan.md
index 871f6aa4..2c1715e3 100644
--- a/docs/development-plan.md
+++ b/docs/development-plan.md
@@ -1014,6 +1014,17 @@ Priority order: E1 → E2 → E3 → E4 → E5 → E6 → E7 → E8 → E9 → E
- Portable standalone scope is Kotlin source files registered in the active session; Java, non-code,
and project-index-only references remain unavailable.
+ - [x] **F5.2** — Inline Anonymous Function / Lambda (Ctrl+Alt+N)
+ - Reuses IDEA K2's `KotlinInlineAnonymousFunctionProcessor` for immediate-call recognition,
+ `LambdaToAnonymousFunctionUtil` for K2-derived lambda signatures, and `CodeInliner` for the
+ final substitution. NetBeans owns the UI, preview, transaction, and undo.
+ - Standalone K2 needs a fresh session after IDEA converts a lambda to an anonymous `fun`: the
+ new PSI otherwise has no FIR mapping. The adapter commits only the final text through
+ `KotlinRefactoringTransaction`, so no intermediate source is persisted and Undo Last
+ Refactoring restores the original document.
+ - 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.
+
- **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 be99a949..f7297e1d 100644
--- a/docs/refactoring-coverage.md
+++ b/docs/refactoring-coverage.md
@@ -14,7 +14,7 @@ netbeans=; test=; milestone=
-
+
@@ -48,7 +48,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, 21 partial, 3 absent, 24 total.
+**Baseline counts:** 0 complete, 22 partial, 2 absent, 24 total.
## Current NetBeans refactorings
@@ -60,6 +60,7 @@ The corresponding record comments above are deliberately machine-readable. `Refa
| `inline-variable-property` | Inline property | Inline (`Ctrl+Alt+N`) | Partial | IDEA code inliner supports local `val` and a local `var` with one declaration initializer and no later writes, invoked from a declaration or selected usage; a later assignment or increment/decrement is rejected before mutation with its expression identified. Receiver/member/accessor, comment, Java, and broader index-backed cases remain. | F3 |
| `inline-function` | Inline function | Inline (`Ctrl+Alt+N`) | Partial | K2 engine supports named functions; complex callable-reference, recursion and Java cases need parity tests. | F3 |
| `inline-type-alias` | Inline Type Alias | Inline (`Ctrl+Alt+N`) | Partial | IDEA K2 type-alias replacement expands Kotlin source aliases from a declaration or usage; the dialog supports all occurrences, this occurrence, and keeping the declaration, while Java/non-code/index-only references remain unavailable. | F5 |
+| `inline-anonymous-function` | Inline Anonymous Function/Lambda | Inline (`Ctrl+Alt+N`) | Partial | Immediately invoked Kotlin lambdas and anonymous functions are inlined through IDEA's K2 signature/conversion and code-inliner engines; the adapter refreshes standalone K2 between those phases and atomically commits final text. Non-invoked functions, Java/non-code/index-only references, and broader IDEA invocation forms remain unavailable. | F5 |
| `extract-function` | Extract Function | `Ctrl+Alt+M` | Partial | Real IDEA generator supports captured parameters, return values, multi-statements and scopes; control-flow, smart-cast, receiver and duplicate matrices remain. | F6 |
| `introduce-variable` | Introduce Variable | `Ctrl+Alt+V` | Partial | Common expression extraction, duplicate replacement and type/`val`/`var` choices work; advanced contexts remain. | F6 |
| `introduce-constant` | Introduce Constant | `Ctrl+Alt+C` | Partial | Compile-time constants and top-level/companion targets work; broader IDEA contexts remain. | F6 |
@@ -80,7 +81,6 @@ The corresponding record comments above are deliberately machine-readable. `Refa
| ID | IDEA family | Why absent | Target |
|---|---|---|---|
-| `inline-anonymous-function` | Inline Anonymous Function/Lambda | The K2 processor is not compiled or adapted to NetBeans. | F5 |
| `move-nested-member-method` | Move nested class/member/method | Requires receiver/visibility/hierarchy conflict support. | F4 |
| `rename-file-package-directory` | Rename file/package/directory | Requires NetBeans filesystem refactoring integration and Kotlin-aware update rules. | F7 |
@@ -109,6 +109,7 @@ The existing NetBeans tests are the initial executable sample set. They intentio
| Inline Function from a call site with multiple usages | `KaInlineFunctionTest` | Exact parity baseline |
| Inline Variable for a write-free local `var`; later assignments/increments rejected | `KaInlineVariableTest` | Portable K2 single-definition baseline; receiver, Java, and index-only write coverage remain incomplete |
| Inline Type Alias from a declaration or a selected usage | `KaInlineTypeAliasTest` | IDEA K2 type substitution baseline; Java/non-code/index-only references remain incomplete |
+| Inline immediately invoked lambda or anonymous function | `KaInlineAnonymousFunctionTest`, `KotlinInlineVariableActionTest` | IDEA K2 call-shape and lambda-signature baseline; standalone K2 session refresh is required between lambda conversion and code inlining |
| Move Declaration with external usage retargeting | `KaMoveDeclarationTest` | Exact parity baseline; transactional source/target undo baseline |
| Copy Declaration into new or existing target | `KaCopyDeclarationTest`, `KotlinRefactoringTransactionTest` | Transactional target creation/replacement and undo baseline |
| Pull Members Up direct target collision | `KaPullMembersUpComputerTest` | Documented standalone limitation beyond direct collision |
diff --git a/docs/stubs.md b/docs/stubs.md
index ce6da7c5..ee1be8c4 100644
--- a/docs/stubs.md
+++ b/docs/stubs.md
@@ -67,6 +67,13 @@ JVM linkage without pulling in the rest of the IDEA refactoring runtime.
| `UsageViewDescriptor` | `Nbm/src/main/java/com/intellij/usageView/` | Compile-only | Interface required by `BaseRefactoringProcessor.createUsageViewDescriptor()` abstract method signature |
| `MoveRenameUsageInfo` | `Nbm/src/main/java/com/intellij/refactoring/util/` | Runtime (shadows `analysis:253`) | Superclass of the Copy Declaration engine's `K2MoveRenameUsageInfo` (E9.19). The real platform ctor calls `PsiDocumentManager.getDocument` and asserts `refEnd <= document.getTextLength()`; the standalone MockProject keeps no live document for mutated PSI, so this stub provides the same ABI (3-arg + 6-arg ctors, `getReferencedElement()`) with **no document access**. `KotlinRefactoring` still compiles against the real `analysis:253` class; this shadows it at runtime because `Nbm` classes load first. |
+**Inline Anonymous Function/Lambda (F5.2)** requires no new class or service stub. IDEA's
+`KotlinInlineAnonymousFunctionProcessor` converts a lambda into fresh anonymous-function PSI and
+then immediately analyses it. A standalone K2 session cannot map that newly inserted PSI to FIR in
+the same session (`FirDeclaration ... fir is null`), so the NetBeans adapter uses the existing IDEA
+`LambdaToAnonymousFunctionUtil` for conversion, refreshes the standalone session, then invokes the
+existing IDEA `CodeInliner`. Only final text is committed through `KotlinRefactoringTransaction`.
+
### Copy Declaration engine port (added for E9.19)
The Copy Declaration multi-declaration path reuses IDEA's real retargeting engine
diff --git a/pom.xml b/pom.xml
index 5e458ecf..48feaee7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.252-SNAPSHOT
+ 0.11.256-SNAPSHOT
pom
Netbeans Plugin Kotlin