diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f865b5a..9179dcd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- 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. - Added Move Kotlin File and Directory refactoring: move the active `.kt` file from **Refactor → Move Kotlin File...**, or right-click a folder and choose **Move Kotlin Directory...** to recursively move its Kotlin files while retaining the selected directory and nested structure below a selected source root/package; matching package directives and supported Kotlin code references are updated, and **Undo Last Refactoring** restores the original locations and contents. - Improved Kotlin Inline Variable: local `var` declarations with one initializer and no subsequent writes can now be inlined from either a declaration or selected usage; assignments and increment/decrement writes are rejected before changing the source with the conflicting expression identified. diff --git a/CoreImpl/pom.xml b/CoreImpl/pom.xml index 4fafda65..47ef404b 100644 --- a/CoreImpl/pom.xml +++ b/CoreImpl/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-core-impl diff --git a/KotlinCompilerCliBase/pom.xml b/KotlinCompilerCliBase/pom.xml index c5739481..01070bbc 100644 --- a/KotlinCompilerCliBase/pom.xml +++ b/KotlinCompilerCliBase/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-compiler-cli-base diff --git a/KotlinFixesImpl/pom.xml b/KotlinFixesImpl/pom.xml index 8c9b4c9e..66817fa4 100644 --- a/KotlinFixesImpl/pom.xml +++ b/KotlinFixesImpl/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-fixes-impl diff --git a/KotlinFormatter/pom.xml b/KotlinFormatter/pom.xml index a77898eb..67a8e35c 100644 --- a/KotlinFormatter/pom.xml +++ b/KotlinFormatter/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-formatter diff --git a/KotlinHighlighting/pom.xml b/KotlinHighlighting/pom.xml index 9b5f2c69..5f19ad8c 100644 --- a/KotlinHighlighting/pom.xml +++ b/KotlinHighlighting/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-highlighting diff --git a/KotlinIcons/pom.xml b/KotlinIcons/pom.xml index 9ef79aaa..0fa07b8a 100644 --- a/KotlinIcons/pom.xml +++ b/KotlinIcons/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-icons diff --git a/KotlinIntentionUtils/pom.xml b/KotlinIntentionUtils/pom.xml index 1d555881..b60e0827 100644 --- a/KotlinIntentionUtils/pom.xml +++ b/KotlinIntentionUtils/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-intention-utils diff --git a/KotlinRefactoring/pom.xml b/KotlinRefactoring/pom.xml index 8326f753..4ca08293 100644 --- a/KotlinRefactoring/pom.xml +++ b/KotlinRefactoring/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-refactoring @@ -279,6 +279,7 @@ org/jetbrains/kotlin/idea/k2/refactoring/inline/KotlinInlinePropertyProcessor.kt org/jetbrains/kotlin/idea/k2/refactoring/inline/KotlinInlineFunctionProcessor.kt + org/jetbrains/kotlin/idea/k2/refactoring/inline/KotlinInlineTypeAliasProcessor.kt org/jetbrains/kotlin/idea/k2/refactoring/inline/KotlinInlineAnonymousFunctionProcessor.kt org/jetbrains/kotlin/idea/k2/refactoring/inline/codeInliner/** org/jetbrains/kotlin/idea/k2/refactoring/util/** diff --git a/KotlinRefactoring/src/main/kotlin/io/github/nbplugins/kotlin/refactoring/KaInlineTypeAliasComputer.kt b/KotlinRefactoring/src/main/kotlin/io/github/nbplugins/kotlin/refactoring/KaInlineTypeAliasComputer.kt new file mode 100644 index 00000000..3180cab5 --- /dev/null +++ b/KotlinRefactoring/src/main/kotlin/io/github/nbplugins/kotlin/refactoring/KaInlineTypeAliasComputer.kt @@ -0,0 +1,161 @@ +/******************************************************************************* + * 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.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.idea.k2.refactoring.inline.codeInliner.TypeAliasUsageReplacementStrategy +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.KtReferenceExpression +import org.jetbrains.kotlin.psi.KtTreeVisitorVoid +import org.jetbrains.kotlin.psi.KtTypeAlias + +/** + * Prepared IDEA-engine data for a Kotlin **Inline Type Alias** refactoring. + * + * @param typeAlias declaration selected for expansion + * @param strategy actual IDEA K2 replacement strategy responsible for type substitution and shortening + * @param usages Kotlin references resolving to [typeAlias], grouped by their session-managed file + * @param triggerUsage reference at the invocation caret, when Inline was started from an alias use + * @param declarationName simple type-alias name for the UI + */ +data class KaInlineTypeAliasResult( + val typeAlias: KtTypeAlias, + val strategy: TypeAliasUsageReplacementStrategy, + val usages: Map>, + val triggerUsage: KtReferenceExpression?, + val declarationName: String, +) + +/** + * User-facing validation failure returned by [KaInlineTypeAliasComputer]. + * + * @param message explanation of why the selected alias cannot be inlined + * @param declarationName alias name, when available + */ +data class KaInlineTypeAliasError( + val message: String, + val declarationName: String, +) + +/** + * Creates a standalone K2 plan for Kotlin **Inline Type Alias**. + * + * The computer uses IDEA's [TypeAliasUsageReplacementStrategy] rather than reconstructing an alias + * expansion as text. NetBeans owns the dialog, preview, persistence, and undo; the IDEA strategy owns + * K2 type substitution, parenthesisation, imports, and reference shortening. + * + * @param cursorKtFile session-managed Kotlin file containing the invocation caret + * @param offset caret offset within [cursorKtFile] + * @param projectKtFiles all Kotlin source files currently registered by the standalone session + */ +class KaInlineTypeAliasComputer( + private val cursorKtFile: KtFile, + private val offset: Int, + private val projectKtFiles: Collection, +) { + + /** Result of [compute]. */ + sealed class Outcome { + /** The target alias and IDEA replacement plan are ready. */ + data class Ready(val result: KaInlineTypeAliasResult) : Outcome() + + /** A type alias was selected but lacks data required by IDEA's replacement engine. */ + data class Error(val error: KaInlineTypeAliasError) : Outcome() + + /** The caret is neither on a type alias declaration nor a reference to one. */ + data object NotApplicable : Outcome() + } + + /** + * Resolves the type alias at the caret and discovers portable Kotlin usages across the active session. + * + * @return [Outcome.Ready] with the IDEA strategy, [Outcome.Error] for an invalid alias, or + * [Outcome.NotApplicable] when no type alias is selected + */ + fun compute(): Outcome { + val element = cursorKtFile.findElementAt(offset) ?: return Outcome.NotApplicable + val triggerUsage = PsiTreeUtil.getParentOfType(element, KtReferenceExpression::class.java, false) + val typeAlias = resolveTypeAliasFromReference(element) + ?: PsiTreeUtil.getParentOfType(element, KtTypeAlias::class.java, false) + ?: return Outcome.NotApplicable + val declarationName = typeAlias.name ?: return Outcome.Error( + KaInlineTypeAliasError("Cannot inline type alias: it has no name.", ""), + ) + if (typeAlias.getTypeReference() == null) { + return Outcome.Error( + KaInlineTypeAliasError( + "Cannot inline type alias '$declarationName': it has no expanded type.", + declarationName, + ), + ) + } + + return Outcome.Ready( + KaInlineTypeAliasResult( + typeAlias = typeAlias, + strategy = TypeAliasUsageReplacementStrategy(typeAlias), + usages = findUsages(typeAlias), + triggerUsage = triggerUsage?.takeIf { resolvesTo(it, typeAlias) }, + declarationName = declarationName, + ), + ) + } + + /** Resolves a reference at the caret to its declared [KtTypeAlias], when applicable. */ + private fun resolveTypeAliasFromReference(element: PsiElement): KtTypeAlias? { + val reference = PsiTreeUtil.getParentOfType(element, KtNameReferenceExpression::class.java, false) + ?: return null + return runCatching { + analyze(reference) { + reference.mainReference?.resolveToSymbol()?.psi as? KtTypeAlias + } + }.getOrNull() + } + + /** Collects K2-resolved Kotlin references to [typeAlias] from every active source file. */ + private fun findUsages(typeAlias: KtTypeAlias): Map> { + val usagesByFile = linkedMapOf>() + for (file in projectKtFiles) { + val usages = mutableListOf() + runCatching { + analyze(file) { + file.accept(object : KtTreeVisitorVoid() { + override fun visitReferenceExpression(expression: KtReferenceExpression) { + super.visitReferenceExpression(expression) + if (resolvesTo(expression, typeAlias)) usages += expression + } + }) + } + } + if (usages.isNotEmpty()) usagesByFile[file] = usages + } + return usagesByFile + } + + /** Checks K2 symbol identity rather than spelling so unrelated same-name aliases are excluded. */ + private fun resolvesTo(reference: KtReferenceExpression, typeAlias: KtTypeAlias): Boolean = + runCatching { + analyze(reference) { + reference.mainReference?.resolveToSymbol()?.psi == typeAlias + } + }.getOrDefault(false) +} diff --git a/Nbm/pom.xml b/Nbm/pom.xml index 73999424..a1261aae 100644 --- a/Nbm/pom.xml +++ b/Nbm/pom.xml @@ -5,7 +5,7 @@ io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT ../pom.xml netbeans-plugin-kotlin-nbm diff --git a/Nbm/src/main/java/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasUI.java b/Nbm/src/main/java/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasUI.java new file mode 100644 index 00000000..d173ab92 --- /dev/null +++ b/Nbm/src/main/java/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasUI.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.nbm.refactoring; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.GridLayout; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +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 Type Alias**. + * + * Users can expand every portable Kotlin usage or only the invocation usage. Expanding all usages + * optionally retains the declaration, mirroring the IDEA type-alias dialog without importing its + * IntelliJ UI and command lifecycle. + * + * @param symbolName display name of the selected type alias + * @param refactoring carrier receiving the selected options + */ +public final class KotlinInlineTypeAliasUI implements RefactoringUI { + + private final String symbolName; + private final KotlinInlineTypeAliasRefactoring refactoring; + + /** + * Creates a type-alias inline UI. + * + * @param symbolName selected alias name, possibly empty before validation + * @param refactoring carrier receiving options + */ + public KotlinInlineTypeAliasUI(String symbolName, KotlinInlineTypeAliasRefactoring refactoring) { + this.symbolName = symbolName; + this.refactoring = refactoring; + } + + @Override + public String getName() { + return "Inline Type Alias"; + } + + @Override + public String getDescription() { + return symbolName == null || symbolName.isEmpty() + ? getName() + : "Inline type alias '" + symbolName + "'"; + } + + @Override + public boolean isQuery() { + return false; + } + + @Override + public CustomRefactoringPanel getPanel(ChangeListener parent) { + return new InlineTypeAliasPanel(refactoring, 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; + } + + /** Panel binding the two supported type-alias inline choices to the carrier. */ + private static final class InlineTypeAliasPanel implements CustomRefactoringPanel { + private final JPanel component = new JPanel(new BorderLayout(0, 8)); + private final KotlinInlineTypeAliasRefactoring refactoring; + private final JRadioButton allOccurrences = new JRadioButton("Inline all occurrences", true); + private final JRadioButton thisOccurrence = new JRadioButton("Inline this occurrence only"); + private final JCheckBox keepDeclaration = new JCheckBox("Keep the type alias declaration"); + + InlineTypeAliasPanel(KotlinInlineTypeAliasRefactoring refactoring, String description) { + this.refactoring = refactoring; + JLabel label = new JLabel(description); + label.setBorder(BorderFactory.createEmptyBorder(8, 12, 0, 12)); + component.add(label, BorderLayout.NORTH); + + JPanel options = new JPanel(new GridLayout(0, 1, 0, 4)); + options.setBorder(BorderFactory.createEmptyBorder(0, 12, 8, 12)); + ButtonGroup group = new ButtonGroup(); + group.add(allOccurrences); + group.add(thisOccurrence); + options.add(allOccurrences); + options.add(thisOccurrence); + options.add(keepDeclaration); + component.add(options, BorderLayout.CENTER); + + allOccurrences.addActionListener(event -> updateOptions()); + thisOccurrence.addActionListener(event -> updateOptions()); + keepDeclaration.addActionListener(event -> updateOptions()); + updateOptions(); + } + + /** Transfers enabled controls to the NetBeans refactoring carrier. */ + private void updateOptions() { + boolean one = thisOccurrence.isSelected(); + keepDeclaration.setEnabled(!one); + if (one) { + keepDeclaration.setSelected(true); + } + refactoring.setInlineThisOnly(one); + refactoring.setKeepDeclaration(keepDeclaration.isSelected()); + } + + @Override public void initialize() { updateOptions(); } + @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 aa1580fb..e9400f3c 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 @@ -32,6 +32,8 @@ import io.github.nbplugins.kotlin.nbm.refactoring.KotlinInlineFunctionRefactoring; import io.github.nbplugins.kotlin.nbm.refactoring.KotlinInlineVariablePlugin; 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.KotlinIntroduceConstantPlugin; import io.github.nbplugins.kotlin.nbm.refactoring.KotlinIntroduceConstantRefactoring; import io.github.nbplugins.kotlin.nbm.refactoring.KotlinCopyDeclarationPlugin; @@ -83,6 +85,7 @@ *
  • {@link SafeDeleteRefactoring} — delegates to {@link KotlinSafeDeletePlugin}
  • *
  • {@link KotlinInlineVariableRefactoring} — delegates to {@link KotlinInlineVariablePlugin}
  • *
  • {@link KotlinInlineFunctionRefactoring} — delegates to {@link KotlinInlineFunctionPlugin}
  • + *
  • {@link KotlinInlineTypeAliasRefactoring} — delegates to {@link KotlinInlineTypeAliasPlugin}
  • *
  • {@link KotlinIntroduceVariableRefactoring} — delegates to {@link KotlinIntroduceVariablePlugin}
  • *
  • {@link KotlinExtractFunctionRefactoring} — delegates to {@link KotlinExtractFunctionPlugin}
  • *
  • {@link KotlinIntroduceConstantRefactoring} — delegates to {@link KotlinIntroduceConstantPlugin}
  • @@ -132,6 +135,9 @@ public RefactoringPlugin createInstance(AbstractRefactoring refactoring) { if (refactoring instanceof KotlinInlineFunctionRefactoring) { return new KotlinInlineFunctionPlugin((KotlinInlineFunctionRefactoring) refactoring); } + if (refactoring instanceof KotlinInlineTypeAliasRefactoring) { + return new KotlinInlineTypeAliasPlugin((KotlinInlineTypeAliasRefactoring) refactoring); + } if (refactoring instanceof KotlinIntroduceVariableRefactoring) { return new KotlinIntroduceVariablePlugin((KotlinIntroduceVariableRefactoring) refactoring); } diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasPlugin.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasPlugin.kt new file mode 100644 index 00000000..94888738 --- /dev/null +++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasPlugin.kt @@ -0,0 +1,225 @@ +/******************************************************************************* + * 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.nbm.refactoring + +import io.github.nbplugins.kotlin.nbm.navigation.KotlinFindUsagesResultElement +import io.github.nbplugins.kotlin.nbm.resolve.KotlinAnalysisAPISession +import io.github.nbplugins.kotlin.refactoring.KaInlineTypeAliasComputer +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtReferenceExpression +import org.jetbrains.kotlin.psi.psiUtil.startOffset +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 + +/** + * Bridges NetBeans refactoring UI to IDEA's K2 **Inline Type Alias** strategy. + * + * @param refactoring document, caret, and UI options chosen by the user + */ +class KotlinInlineTypeAliasPlugin( + private val refactoring: KotlinInlineTypeAliasRefactoring, +) : ProgressProviderAdapter(), RefactoringPlugin { + + override fun preCheck(): Problem? = null + override fun fastCheckParameters(): Problem? = null + override fun checkParameters(): Problem? = null + override fun cancelRequest() {} + + /** + * Builds portable Kotlin preview items and one transactional apply element. + * + * @param bag NetBeans element bag to populate + * @return fatal validation problem, or `null` when a valid plan was prepared + */ + override fun prepare(bag: RefactoringElementsBag): Problem? { + val doc = refactoring.doc + val sourceFile = ProjectUtils.getFileObjectForDocument(doc) ?: return null + val nbProject = ProjectUtils.getKotlinProjectForFileObject(sourceFile) ?: return null + val session = KotlinAnalysisAPISession.getSession(nbProject) + val ktFile = session.getKtFileForPath(sourceFile.path) ?: return null + val projectKtFiles = session.session.modulesWithFiles.values.flatten().filterIsInstance() + + return when (val outcome = KaInlineTypeAliasComputer(ktFile, refactoring.offset, projectKtFiles).compute()) { + is KaInlineTypeAliasComputer.Outcome.NotApplicable -> null + is KaInlineTypeAliasComputer.Outcome.Error -> Problem(true, outcome.error.message) + is KaInlineTypeAliasComputer.Outcome.Ready -> { + val selectedUsages = selectedUsages(outcome.result.usages, outcome.result.triggerUsage) + if (refactoring.inlineThisOnly && selectedUsages.isEmpty()) { + return Problem(true, "Cannot inline this occurrence: the type alias usage is no longer available.") + } + selectedUsages.forEach { (usageFile, usages) -> + val usageFo = fileObjectFor(usageFile) ?: return@forEach + usages.forEach { usage -> + runCatching { + bag.add( + refactoring, + KotlinFindUsagesResultElement( + OffsetRange(usage.textRange.startOffset, usage.textRange.endOffset), + usageFo, + ), + ) + } + } + } + val declarationFile = fileObjectFor(outcome.result.typeAlias.containingKtFile) ?: sourceFile + bag.add( + refactoring, + KotlinInlineTypeAliasApplyElement(declarationFile, nbProject, sourceFile.path, refactoring), + ) + null + } + } + } + + /** Limits the plan to its trigger reference only when the UI chose that mode. */ + private fun selectedUsages( + allUsages: Map>, + triggerUsage: KtReferenceExpression?, + ): Map> { + if (!refactoring.inlineThisOnly) return allUsages + val trigger = triggerUsage ?: return emptyMap() + val file = trigger.containingKtFile + return mapOf(file to listOf(trigger)) + } + + /** Converts a session virtual path to a writable NetBeans file. */ + private fun fileObjectFor(file: KtFile): FileObject? = + file.virtualFile?.path?.let { FileUtil.toFileObject(FileUtil.normalizeFile(java.io.File(it))) } +} + +/** + * Performs an Inline Type Alias change as a hunk-preserving NetBeans transaction. + * + * @param declarationFile parent file used by the preview hierarchy + * @param nbProject NetBeans project owning the standalone K2 session + * @param cursorFilePath source file path at action invocation + * @param refactoring carrier with mode options + */ +class KotlinInlineTypeAliasApplyElement( + private val declarationFile: FileObject, + private val nbProject: org.netbeans.api.project.Project, + private val cursorFilePath: String, + private val refactoring: KotlinInlineTypeAliasRefactoring, +) : SimpleRefactoringElementImplementation() { + + private var transaction: KotlinRefactoringTransaction? = null + + override fun getText(): String = "Inline type alias" + override fun getDisplayText(): String = text + override fun getLookup(): Lookup = Lookups.fixed(declarationFile) + override fun getParentFile(): FileObject = declarationFile + + override fun getPosition(): PositionBounds? = try { + val support = DataObject.find(declarationFile).lookup.lookup(CloneableEditorSupport::class.java) ?: return null + PositionBounds(support.createPositionRef(0, Bias.Forward), support.createPositionRef(0, Bias.Backward)) + } catch (_: Exception) { + null + } + + /** Recomputes and executes the IDEA strategy, committing all generated document text together. */ + override fun performChange() { + var pending: KotlinRefactoringTransaction? = null + try { + val session = KotlinAnalysisAPISession.getSession(nbProject) + val cursorKtFile = session.getKtFileForPath(cursorFilePath) ?: return + val projectKtFiles = session.session.modulesWithFiles.values.flatten().filterIsInstance() + val ready = KaInlineTypeAliasComputer(cursorKtFile, refactoring.offset, projectKtFiles).compute() + as? KaInlineTypeAliasComputer.Outcome.Ready ?: return + val result = ready.result + val usages = chooseUsages(result.usages, result.triggerUsage) + if (refactoring.inlineThisOnly && usages.isEmpty()) return + + val affectedKtFiles = linkedSetOf() + affectedKtFiles += usages.keys + if (!refactoring.inlineThisOnly && !refactoring.keepDeclaration) { + affectedKtFiles += result.typeAlias.containingKtFile + } + val affectedFiles = affectedKtFiles.mapNotNull { ktFile -> + fileObjectFor(ktFile)?.let { file -> ktFile to file } + }.toMap(linkedMapOf()) + if (affectedFiles.size != affectedKtFiles.size) return + + val currentTransaction = KotlinRefactoringTransaction() + pending = currentTransaction + affectedFiles.values.forEach(currentTransaction::captureExisting) + + for ((_, references) in usages) { + for (usage in references.sortedForInline()) { + if (!usage.isValid) continue + result.strategy.createReplacer(usage)?.invoke() + } + } + if (!refactoring.inlineThisOnly && !refactoring.keepDeclaration) result.typeAlias.delete() + + affectedKtFiles.forEach { ktFile -> + val file = affectedFiles.getValue(ktFile) + currentTransaction.stageHunkText(file, ktFile.text, nbProject) + } + currentTransaction.commit() + transaction = currentTransaction + pending = null + KotlinAnalysisAPISession.invalidate(nbProject) + } catch (_: Throwable) { + runCatching { pending?.rollback() } + KotlinAnalysisAPISession.invalidate(nbProject) + } + } + + /** Restores all edited files and invalidates cached standalone PSI. */ + override fun undoChange() { + runCatching { transaction?.undo() } + KotlinAnalysisAPISession.invalidate(nbProject) + } + + /** Applies the UI's all-usages/this-occurrence selection to fresh K2 references. */ + private fun chooseUsages( + allUsages: Map>, + triggerUsage: KtReferenceExpression?, + ): Map> { + if (!refactoring.inlineThisOnly) return allUsages + val trigger = triggerUsage ?: return emptyMap() + return mapOf(trigger.containingKtFile to listOf(trigger)) + } + + /** Resolves a K2 file path to its NetBeans file counterpart. */ + private fun fileObjectFor(file: KtFile): FileObject? = + file.virtualFile?.path?.let { FileUtil.toFileObject(FileUtil.normalizeFile(java.io.File(it))) } + + /** Orders nested references exactly as IDEA's `UsageReplacementStrategy` does. */ + private fun List.sortedForInline(): List = + sortedWith { first, second -> + if (first.parent.textRange.intersects(second.parent.textRange)) { + compareValuesBy(second, first) { it.startOffset } + } else { + compareValuesBy(first, second) { it.startOffset } + } + } +} diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasRefactoring.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasRefactoring.kt new file mode 100644 index 00000000..656ae681 --- /dev/null +++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KotlinInlineTypeAliasRefactoring.kt @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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.nbm.refactoring + +import org.netbeans.modules.refactoring.api.AbstractRefactoring +import org.openide.util.lookup.Lookups +import javax.swing.text.StyledDocument + +/** + * Carries Kotlin **Inline Type Alias** inputs and user-selected options through NetBeans refactoring UI. + * + * @param doc active Kotlin editor document + * @param offset caret offset in [doc] + */ +class KotlinInlineTypeAliasRefactoring( + val doc: StyledDocument, + val offset: Int, +) : AbstractRefactoring(Lookups.fixed(doc, Integer.valueOf(offset))) { + + /** True when only the reference at [offset] is expanded. */ + var inlineThisOnly: Boolean = false + + /** True when the alias declaration remains after all selected references are expanded. */ + var keepDeclaration: Boolean = false +} 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 6b3fdd2e..4f216387 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 @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.log.KotlinLogger 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.KtFile import org.jetbrains.kotlin.psi.KtNamedDeclaration import com.intellij.psi.PsiElement @@ -37,12 +38,13 @@ import javax.swing.text.JTextComponent import javax.swing.text.StyledDocument /** - * Editor action for **Ctrl+Alt+N** — "Inline" for Kotlin `fun` declarations and `val`/`var` properties. + * Editor action for **Ctrl+Alt+N** — "Inline" for Kotlin functions, properties, and type aliases. * * 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] * * 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 @@ -76,6 +78,10 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR KotlinInlineVariableUI(targetDeclaration.name.orEmpty(), KotlinInlineVariableRefactoring(doc, targetOffset)), TopComponent.getRegistry().activated, ) + is KtTypeAlias -> UI.openRefactoringUI( + KotlinInlineTypeAliasUI(targetDeclaration.name.orEmpty(), KotlinInlineTypeAliasRefactoring(doc, targetOffset)), + TopComponent.getRegistry().activated, + ) null -> return@runCatching } }.onFailure { e -> @@ -87,13 +93,13 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR * Resolves the directly selected inline declaration. * * A reference resolves through K2 to its declaration; a declaration-position caret uses the - * directly selected property/function token. Unlike the former function-first lookup, this - * never treats an arbitrary child of an enclosing function as the function itself. + * directly selected property/function/type-alias token. Unlike the former function-first lookup, + * this never treats an arbitrary child of an enclosing function as the function itself. * * @param doc Kotlin editor document containing the selection * @param selectionStart first selected offset, or the caret offset when empty * @param selectionEnd offset immediately after the selection, or [selectionStart] when empty - * @return the target property/function, or `null` when the selection has no inline target + * @return the target property/function/type alias, or `null` when the selection has no inline target */ private fun resolveTargetAt( doc: StyledDocument, @@ -116,7 +122,7 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR resolveTargetAt(ktFile, offset, offset) /** - * Resolves the property/function covered by a caret or selection in a session-managed Kotlin file. + * Resolves the property/function/type alias covered by a caret or selection in a session-managed Kotlin file. * * @param selectionStart first selected offset, or the caret offset for an empty selection * @param selectionEnd offset immediately after the selection, or [selectionStart] when empty @@ -135,7 +141,9 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR for (offset in offsets) { val element = ktFile.findElementAt(offset) ?: continue resolveReferenceTarget(element)?.let { return it } - ((element.parent as? KtProperty) ?: (element.parent as? KtNamedFunction))?.let { return it } + ((element.parent as? KtProperty) + ?: (element.parent as? KtNamedFunction) + ?: (element.parent as? KtTypeAlias))?.let { return it } } return null } @@ -148,7 +156,7 @@ class KotlinInlineVariableAction : BaseAction(ACTION_NAME, SAVE_POSITION or ABBR analyze(reference) { reference.mainReference?.resolveToSymbol()?.psi as? KtNamedDeclaration } - }.getOrNull()?.takeIf { it is KtProperty || it is KtNamedFunction } + }.getOrNull()?.takeIf { it is KtProperty || it is KtNamedFunction || it is KtTypeAlias } } companion object { diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KaInlineTypeAliasTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KaInlineTypeAliasTest.kt new file mode 100644 index 00000000..69ea28ef --- /dev/null +++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/refactoring/KaInlineTypeAliasTest.kt @@ -0,0 +1,146 @@ +/******************************************************************************* + * 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.KaInlineTypeAliasComputer +import org.jetbrains.kotlin.psi.KtFile +import utils.KotlinTestCase +import java.nio.file.Files +import java.nio.file.Path + +/** Tests IDEA K2 strategy preparation and application for Kotlin Inline Type Alias. */ +class KaInlineTypeAliasTest : KotlinTestCase("KaInlineTypeAliasTest", "inlineTypeAlias") { + + 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() } + + /** Builds a real K2 session backed by one fixture and returns its computer/file/temp directory. */ + 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 cleanSource = source.replace(CARET, "") + val tempDirectory = Files.createTempDirectory("nbkotlin-inline-type-alias-$fixture") + val tempFile = tempDirectory.resolve("file.kt") + Files.writeString(tempFile, cleanSource) + val session = KotlinAnalysisAPISession.createWithJars( + moduleName = "inline-type-alias-$fixture", + binaryJars = listOf(stdlib), + sourceRoots = listOf(tempDirectory), + ) + val ktFile = session.getKtFileForPath(tempFile.toString()) ?: error("Missing KtFile for $tempFile") + val files = session.session.modulesWithFiles.values.flatten().filterIsInstance() + return Triple(KaInlineTypeAliasComputer(ktFile, marker, files), ktFile, tempDirectory) + } + + /** A declaration caret produces a ready plan with its Kotlin usage. */ + fun testDeclarationCaret_collectsUsage() { + val (computer, _, temporaryDirectory) = prepare("simple") ?: return + try { + val outcome = computer.compute() + assertTrue("Expected ready result, got $outcome", outcome is KaInlineTypeAliasComputer.Outcome.Ready) + val result = (outcome as KaInlineTypeAliasComputer.Outcome.Ready).result + assertEquals("Name", result.declarationName) + assertEquals(1, result.usages.values.sumOf { it.size }) + assertTrue("Declaration invocation must not retain a usage trigger", result.triggerUsage == null) + } finally { + temporaryDirectory.toFile().deleteRecursively() + } + } + + /** A usage caret resolves back to the same alias and records the exact trigger reference. */ + fun testUsageCaret_resolvesAliasAndRecordsTrigger() { + val (computer, _, temporaryDirectory) = prepare("caretOnUsage") ?: return + try { + val outcome = computer.compute() + assertTrue("Expected ready result, got $outcome", outcome is KaInlineTypeAliasComputer.Outcome.Ready) + val result = (outcome as KaInlineTypeAliasComputer.Outcome.Ready).result + assertEquals("Name", result.declarationName) + assertTrue("Usage invocation must retain its trigger", result.triggerUsage != null) + assertEquals("Name", result.triggerUsage?.text) + } finally { + temporaryDirectory.toFile().deleteRecursively() + } + } + + /** IDEA's real type-alias replacement strategy expands a simple alias and allows declaration deletion. */ + fun testApply_simpleAlias_expandsUsageAndDeletesDeclaration() { + val (computer, ktFile, temporaryDirectory) = prepare("simple") ?: return + try { + val ready = computer.compute() as? KaInlineTypeAliasComputer.Outcome.Ready + assertTrue("Expected ready alias plan, got $ready", ready != null) + val result = ready!!.result + result.usages.values.flatten().forEach { usage -> result.strategy.createReplacer(usage)?.invoke() } + result.typeAlias.delete() + + assertFalse("Alias declaration must be deleted: ${ktFile.text}", ktFile.text.contains("typealias Name")) + assertTrue("Usage must expand to String: ${ktFile.text}", ktFile.text.contains("name: String")) + } finally { + temporaryDirectory.toFile().deleteRecursively() + } + } + + /** IDEA's strategy substitutes the concrete arguments of each generic alias use independently. */ + fun testApply_genericAlias_preservesPerUsageSubstitutions() { + val (computer, ktFile, temporaryDirectory) = prepare("generic") ?: return + try { + val ready = computer.compute() as? KaInlineTypeAliasComputer.Outcome.Ready + assertNotNull("Expected ready generic alias plan", ready) + val result = ready!!.result + result.usages.values.flatten().forEach { usage -> result.strategy.createReplacer(usage)?.invoke() } + result.typeAlias.delete() + + assertTrue( + "String use must stay substituted: ${ktFile.text}", + ktFile.text.contains("List") || ktFile.text.contains("List"), + ) + assertTrue( + "Int use must stay substituted: ${ktFile.text}", + ktFile.text.contains("List") || ktFile.text.contains("List"), + ) + assertFalse("Alias declaration must be deleted: ${ktFile.text}", ktFile.text.contains("typealias Names")) + } finally { + temporaryDirectory.toFile().deleteRecursively() + } + } + + /** The upstream processor must link so a future full IDEA processor route remains available. */ + fun testIdeaInlineTypeAliasProcessor_isLinkable() { + try { + Class.forName( + "org.jetbrains.kotlin.idea.k2.refactoring.inline.KotlinInlineTypeAliasProcessor", + true, + javaClass.classLoader, + ) + } catch (error: NoClassDefFoundError) { + fail("KotlinInlineTypeAliasProcessor cannot link: ${error.message}") + } catch (error: ClassNotFoundException) { + fail("KotlinInlineTypeAliasProcessor 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 91158879..566cec7e 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 @@ -20,6 +20,7 @@ import io.github.nbplugins.kotlin.nbm.resolve.KotlinAnalysisAPISession import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtTypeAlias import org.netbeans.junit.NbTestCase import java.nio.file.Files import java.nio.file.Path @@ -155,6 +156,36 @@ class KotlinInlineVariableActionTest : NbTestCase("KotlinInlineVariableActionTes } } + /** A type-alias declaration remains a valid unified Inline target. */ + fun testTypeAliasDeclaration_resolvesTypeAlias() { + val source = """ + typealias Name = String + + fun greet(name: Name) = name + """.trimIndent() + withKtFile(source) { ktFile -> + val target = KotlinInlineVariableAction().resolveTargetAt(ktFile, source.indexOf("Name =")) + + assertTrue("Expected a type alias target, got $target", target is KtTypeAlias) + assertEquals("Name", (target as KtTypeAlias).name) + } + } + + /** A type-alias usage must resolve back to its type-alias declaration. */ + fun testTypeAliasUsage_resolvesTypeAlias() { + val source = """ + typealias Name = String + + fun greet(name: Name) = name + """.trimIndent() + withKtFile(source) { ktFile -> + val target = KotlinInlineVariableAction().resolveTargetAt(ktFile, source.lastIndexOf("Name")) + + assertTrue("Expected a type alias target, got $target", target is KtTypeAlias) + assertEquals("Name", (target as KtTypeAlias).name) + } + } + /** A body expression with no target declaration must not accidentally select `main`. */ fun testUnrelatedBodyExpression_returnsNull() { 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 e356e738..75d1b796 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, 20 partial, 4 absent, 24 total." + val totals = "**Baseline counts:** 0 complete, 21 partial, 3 absent, 24 total." assertTrue("Visible baseline totals must match the records", Files.readString(matrix).contains(totals)) - assertEquals(20, records.count { it.status == "partial" }) - assertEquals(4, records.count { it.status == "absent" }) + assertEquals(21, records.count { it.status == "partial" }) + assertEquals(3, records.count { it.status == "absent" }) } /** Finds the checkout root from Surefire's module working directory. */ diff --git a/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/caretOnUsage/file.kt b/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/caretOnUsage/file.kt new file mode 100644 index 00000000..fd865d86 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/caretOnUsage/file.kt @@ -0,0 +1,5 @@ +package inlineTypeAlias.caretOnUsage + +typealias Name = String + +fun greet(name: Name) = "Hello, $name" diff --git a/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/generic/file.kt b/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/generic/file.kt new file mode 100644 index 00000000..3359c4e6 --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/generic/file.kt @@ -0,0 +1,6 @@ +package inlineTypeAlias.generic + +typealias Names = List + +val strings: Names = listOf("one") +val numbers: Names = listOf(1) diff --git a/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/simple/file.kt b/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/simple/file.kt new file mode 100644 index 00000000..f4d0befb --- /dev/null +++ b/Nbm/src/test/resources/projForTest/src/inlineTypeAlias/simple/file.kt @@ -0,0 +1,5 @@ +package inlineTypeAlias.simple + +typealias Name = String + +fun greet(name: Name) = "Hello, $name" diff --git a/docs/development-plan.md b/docs/development-plan.md index b6c0e88d..871f6aa4 100644 --- a/docs/development-plan.md +++ b/docs/development-plan.md @@ -1003,6 +1003,17 @@ Priority order: E1 → E2 → E3 → E4 → E5 → E6 → E7 → E8 → E9 → E **E9 is complete.** + - [x] **F5.1** — Inline Type Alias (Ctrl+Alt+N) + - Reuses IDEA K2's `TypeAliasUsageReplacementStrategy` and compiled + `KotlinInlineTypeAliasProcessor`; NetBeans deliberately owns the dialog, preview, transaction, + and undo instead of invoking IDEA's processor lifecycle. + - The unified Inline action resolves a type-alias declaration or usage; its NetBeans panel supports + all occurrences, the invocation occurrence only, and retaining the declaration. K2-generated + type substitutions/import changes are committed through `KotlinRefactoringTransaction` so Undo + Last Refactoring restores every touched Kotlin document. + - Portable standalone scope is Kotlin source files registered in the active session; Java, non-code, + and project-index-only references 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 91fbf33f..be99a949 100644 --- a/docs/refactoring-coverage.md +++ b/docs/refactoring-coverage.md @@ -13,7 +13,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, 20 partial, 4 absent, 24 total. +**Baseline counts:** 0 complete, 21 partial, 3 absent, 24 total. ## Current NetBeans refactorings @@ -59,6 +59,7 @@ The corresponding record comments above are deliberately machine-readable. `Refa | `change-signature` | Change Signature | `Ctrl+F6` | Partial | K2 signature engine handles calls, overrides, constructors, destructuring and operators; every touched document now commits or rolls back through one hunk-preserving transaction, while broader IDEA scenarios remain. | F1 | | `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 | | `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 | @@ -79,7 +80,6 @@ The corresponding record comments above are deliberately machine-readable. `Refa | ID | IDEA family | Why absent | Target | |---|---|---|---| -| `inline-type-alias` | Inline Type Alias | The K2 processor is not compiled or adapted to NetBeans. | F5 | | `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 | @@ -108,6 +108,7 @@ The existing NetBeans tests are the initial executable sample set. They intentio | Change Signature across declaration and separate call site | `KaChangeSignatureTest` | Exact parity baseline | | 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 | | 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/pom.xml b/pom.xml index 6cdb1b89..cbc9d211 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.github.nbplugins netbeans-plugin-kotlin - 0.11.249-SNAPSHOT + 0.11.250-SNAPSHOT pom Netbeans Plugin Kotlin