Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- Fixed Move Nested Member to preserve unsaved source, target, and usage edits, reject unsafe `inner` and self-descendant targets, and report failed applies; Undo Last Refactoring remains available only after a successful move.
- Fixed Kotlin file and package-directory renames to show one immediate **Refactor** submenu in the Projects/Files context menu, treat unchanged names as no-ops, preserve requested spelling for case-only renames with Undo Last Refactoring, and retain each open editor caret when a Change Kotlin Package operation is undone.
- Added structural conversion hints for Kotlin: with the caret on an operator or convention-named call you can now convert `a.plus(b)` to `a + b`, `a.get(i)`/`a.set(i, v)` to `a[i]`/`a[i] = v`, `a.contains(b)` to `b in a`, `a.invoke(b)` to `a(b)`, `a.not()`/`a.unaryPlus()` to `!a`/`+a`, and `f() {}` to `f {}` directly from the hint popup.
# 0.11.54 (2026-10-12)

Expand Down
2 changes: 1 addition & 1 deletion CoreImpl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-core-impl</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinCodeInsightImpl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-code-insight-impl</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinCompilerCliBase/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-compiler-cli-base</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinFixesImpl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-fixes-impl</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinFormatter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-formatter</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinHighlighting/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-highlighting</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinIcons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-icons</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinIntentionUtils/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-intention-utils</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion KotlinRefactoring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-refactoring</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.jetbrains.kotlin.idea.k2.refactoring.move.KotlinMoveUsageSearchServic
import org.jetbrains.kotlin.idea.k2.refactoring.move.processor.usages.K2MoveRenameUsageInfo
import org.jetbrains.kotlin.idea.references.KtReference
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
Expand Down Expand Up @@ -176,7 +177,8 @@ class KaMoveNestedMemberComputer(
.asSequence()
.filter { target ->
target.name != null &&
(target.containingKtFile.virtualFile?.path to target.textOffset) !in excludedOwners
(target.containingKtFile.virtualFile?.path to target.textOffset) !in excludedOwners &&
!isDescendantTarget(declaration, target)
}
.filter { target ->
when (kind) {
Expand Down Expand Up @@ -299,14 +301,20 @@ class KaMoveNestedMemberComputer(

/** Identifies only relocations whose receiver/qualification change is safe in standalone K2. */
private fun supportedKind(declaration: KtNamedDeclaration): Kind? = when (declaration) {
is KtClassOrObject -> declaration.takeIf { it.getStrictParentOfType<KtClassOrObject>() != null }
?.let { Kind.NESTED_CLASS }
is KtClassOrObject -> declaration.takeIf {
it.getStrictParentOfType<KtClassOrObject>() != null &&
(it !is KtClass || !it.isInner())
}?.let { Kind.NESTED_CLASS }
is KtNamedFunction, is KtProperty -> declaration.takeIf {
it.getStrictParentOfType<KtObjectDeclaration>()?.isCompanion() == true
}?.let { Kind.COMPANION_MEMBER }
else -> null
}

/** Returns whether [target] is nested in [declaration] and would be deleted with it. */
private fun isDescendantTarget(declaration: KtNamedDeclaration, target: KtClassOrObject): Boolean =
com.intellij.psi.util.PsiTreeUtil.isAncestor(declaration, target, false)

/** Locates the named class/object chosen by the NetBeans target-selection controller. */
private fun resolveTarget(targetFile: KtFile, targetOffset: Int): KtClassOrObject? =
targetFile.findElementAt(targetOffset)?.getNonStrictParentOfType<KtClassOrObject>()
Expand All @@ -318,6 +326,9 @@ class KaMoveNestedMemberComputer(
?: return listOf("Move source owner is unavailable.")
val messages = mutableListOf<String>()
if (sourceOwner == target) messages += "The target class already owns ${declaration.name}."
if (isDescendantTarget(declaration, target)) {
messages += "The target cannot be inside the declaration being moved."
}
if (kind == Kind.COMPANION_MEMBER && target.companionObjects.singleOrNull() == null) {
messages += "Target ${target.name ?: "class"} must declare exactly one companion object."
}
Expand Down
2 changes: 1 addition & 1 deletion Nbm/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-kotlin</artifactId>
<version>0.11.284-SNAPSHOT</version>
<version>0.11.288-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>netbeans-plugin-kotlin-nbm</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.netbeans.modules.refactoring.spi.ui.UI;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.util.Lookup;
import org.openide.util.lookup.Lookups;
import org.openide.windows.TopComponent;
Expand All @@ -55,18 +56,20 @@
public class KotlinActionsImplementationProvider extends ActionsImplementationProvider {

/**
* Returns whether the context is an eligible Kotlin package folder or an open Kotlin editor.
* Returns whether the context is an eligible Kotlin package folder, physical Kotlin file, or open Kotlin editor.
*
* @param lookup lookup supplied by the Refactor action
* @return {@code true} when this provider owns the Rename operation
*/
@Override
public boolean canRename(Lookup lookup) {
return KotlinProjectFolderRenameSelection.INSTANCE.resolve(lookup) != null || canRenameEditor(lookup);
return KotlinProjectFolderRenameSelection.INSTANCE.resolve(lookup) != null
|| selectedKotlinFile(lookup) != null
|| canRenameEditor(lookup);
}

/**
* Opens physical directory rename for Kotlin package folders or symbol rename for Kotlin editors.
* Opens physical file/directory rename for a project-tree selection or symbol rename for an editor selection.
*
* @param lookup lookup supplied by the Refactor action
*/
Expand All @@ -79,19 +82,54 @@ public void doRename(Lookup lookup) {
}

EditorCookie ec = lookup.lookup(EditorCookie.class);
JEditorPane pane = ec.getOpenedPanes()[0];
// Use selection start so that renaming a fully-selected word positions inside the word,
// not past it. When no selection, getSelectionStart() == getCaretPosition().
int caretPosition = pane.getSelectionStart();
StyledDocument doc = ec.getDocument();
FileObject fo = ProjectUtils.getFileObjectForDocument(doc);
final KtFile ktFile = KotlinPsiManager.INSTANCE.getParsedFile(fo);
final PsiElement psi = ktFile.findElementAt(caretPosition);
// Do NOT include fo (FileObject) in the lookup — a built-in NetBeans rename plugin
// would see it and rename the file on disk instead of renaming the code symbol.
UI.openRefactoringUI(
new KotlinRenameRefactoringUI(psi, new RenameRefactoring(Lookups.fixed(psi, doc, caretPosition))),
TopComponent.getRegistry().getActivated());
if (ec != null && ec.getOpenedPanes() != null && ec.getOpenedPanes().length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve filesystem rename for open project nodes

When a Kotlin file is already open and Rename is invoked from its Projects/Files node, that DataNode lookup exposes an EditorCookie with nonempty opened panes, so this branch consumes the node request and launches symbol rename at the editor caret instead of reaching selectedKotlinFile() for physical rename. Fresh evidence after the earlier branch-order fix is that an open project node carries both node and editor cookies, so opened-pane state still cannot identify the invocation source; check the selected-node/Explorer context before choosing the editor route.

Useful? React with 👍 / 👎.

JEditorPane pane = ec.getOpenedPanes()[0];
// Use selection start so that renaming a fully-selected word positions inside the word,
// not past it. When no selection, getSelectionStart() == getCaretPosition().
int caretPosition = pane.getSelectionStart();
StyledDocument doc = ec.getDocument();
FileObject fo = doc == null ? null : ProjectUtils.getFileObjectForDocument(doc);
final KtFile ktFile = fo == null ? null : KotlinPsiManager.INSTANCE.getParsedFile(fo);
final PsiElement psi = ktFile == null ? null : ktFile.findElementAt(caretPosition);
if (psi != null) {
// Do NOT include fo (FileObject) in the lookup — a built-in NetBeans rename plugin
// would see it and rename the file on disk instead of renaming the code symbol.
UI.openRefactoringUI(
new KotlinRenameRefactoringUI(psi, new RenameRefactoring(Lookups.fixed(psi, doc, caretPosition))),
TopComponent.getRegistry().getActivated());
return;
}
}

FileObject file = selectedKotlinFile(lookup);
if (file != null) {
openFileRename(file);
}
}

/** Opens the explicit filesystem rename UI for one Kotlin file selected in a project tree. */
private void openFileRename(FileObject file) {
EventQueue.invokeLater(() -> UI.openRefactoringUI(
new KotlinFilesystemRenameUI(
new io.github.nbplugins.kotlin.nbm.refactoring.KotlinRenameFileRefactoring(file, file.getName()),
false,
file.getNameExt()),
TopComponent.getRegistry().getActivated()));
}

/**
* Resolves one physical Kotlin source file from a file-node lookup without requiring an editor.
*
* @param lookup lookup supplied by NetBeans
* @return selected valid Kotlin file, or {@code null} for folders and unrelated selections
*/
private FileObject selectedKotlinFile(Lookup lookup) {
FileObject file = lookup.lookup(FileObject.class);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject multi-selection before choosing a Kotlin file

When at least two unopened Kotlin file nodes are selected, NetBeans' merged node lookup makes lookup(FileObject.class) return one match rather than reporting ambiguity. canRename() consequently returns true and doRename() opens physical rename for an arbitrary file, whereas the surrounding node actions require exactly one node; validate lookupAll(Node.class) or equivalent file cardinality before returning a target.

Useful? React with 👍 / 👎.

if (file == null) {
DataObject dataObject = lookup.lookup(DataObject.class);
file = dataObject == null ? null : dataObject.getPrimaryFile();
}
return file != null && file.isValid() && !file.isFolder() && file.hasExt("kt") ? file : null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ import org.openide.text.NbDocument
* @param file transaction participant to update
* @param finalText resulting text produced by the K2 refactoring engine
* @param project Kotlin project whose formatter settings apply to each changed hunk
* @param caretOffset pre-refactoring offset to restore after native editor undo, or `null` when the
* file has no open editor pane
*/
internal fun KotlinRefactoringTransaction.stageHunkText(
file: FileObject,
finalText: String,
project: Project,
caretOffset: Int? = null,
) {
stageText(file, finalText) { document, originalText, targetText ->
val hunks = TextRangeDiff.computeHunks(originalText, targetText).sortedByDescending { it.oldStart }
Expand All @@ -57,6 +60,7 @@ internal fun KotlinRefactoringTransaction.stageHunkText(
}
}
}
if (hunks.isNotEmpty()) caretOffset?.let { joinCaretRestoreOnUndo(document, file, it) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ 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.cookies.EditorCookie
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
Expand Down Expand Up @@ -135,7 +137,7 @@ class KotlinChangePackageApplyElement(
val originalText = outcome.originalTexts[path]
?: error("Change Kotlin Package has no original text snapshot for $path.")
current.captureExisting(file, originalText)
current.stageText(file, text)
current.stageHunkText(file, text, project, openCaretOffset(file))
}
current.commit()
transaction = current
Expand All @@ -148,6 +150,17 @@ class KotlinChangePackageApplyElement(
KotlinAnalysisAPISession.invalidate(project)
}

/**
* Returns the caret offset from an already-open editor for [file] without opening a new editor.
*
* Native undo must restore each modified open editor independently; unopened files do not have a
* user-visible caret and therefore deliberately receive no restoration edit.
*/
private fun openCaretOffset(file: FileObject): Int? = runCatching {
val cookie = DataObject.find(file).lookup.lookup(EditorCookie::class.java) ?: return null
cookie.openedPanes?.firstOrNull()?.caretPosition
}.getOrNull()

/** Restores all original package directives and usage texts. */
override fun undoChange() {
runCatching {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class KotlinMoveNestedMemberAction : BaseAction(ACTION_NAME, SAVE_POSITION or AB
// Kotlin files created or saved since the previous editor analysis are selectable.
KotlinAnalysisAPISession.invalidate(project)
val session = KotlinAnalysisAPISession.getSession(project)
session.updateFileContent(source.path, document.getText(0, document.length))
session.synchronizeOpenDocuments()
val sourcePsi = session.getKtFileForPath(source.path)
?: error("Move Nested Member could not resolve the Kotlin source file.")
val computer = KaMoveNestedMemberComputer(sourcePsi, target.caretPosition)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class KotlinMoveNestedMemberPlugin(

KotlinAnalysisAPISession.invalidate(project)
val session = KotlinAnalysisAPISession.getSession(project)
session.synchronizeOpenDocuments()
val sourcePsi = session.getKtFileForPath(source.path)
?: return Problem(true, "Move Nested Member could not resolve the Kotlin source PSI.")
val targetPsi = session.getKtFileForPath(target.path)
Expand Down Expand Up @@ -117,11 +118,12 @@ private class KotlinMoveNestedMemberApplyElement(
/** Refreshes K2 PSI, stages every returned final text, and commits them atomically. */
override fun performChange() {
var pending: KotlinRefactoringTransaction? = null
runCatching {
try {
val request = refactoring.request() ?: error("Move Nested Member target is incomplete.")
val target = fileObject(request.filePath) ?: error("Move Nested Member could not resolve target file.")
KotlinAnalysisAPISession.invalidate(project)
val session = KotlinAnalysisAPISession.getSession(project)
session.synchronizeOpenDocuments()
val sourcePsi = session.getKtFileForPath(sourceFile.path)
?: error("Move Nested Member could not refresh source Kotlin PSI.")
val targetPsi = session.getKtFileForPath(target.path)
Expand Down Expand Up @@ -149,10 +151,18 @@ private class KotlinMoveNestedMemberApplyElement(
error("Move Nested Member is no longer applicable.")
is KaMoveNestedMemberComputer.Apply.Error -> throw result.error
}
}.onFailure { KotlinLogger.INSTANCE.logException("Move Nested Member apply failed", it) }
runCatching { pending?.rollback() }
.onFailure { KotlinLogger.INSTANCE.logException("Move Nested Member rollback failed", it) }
KotlinAnalysisAPISession.invalidate(project)
} catch (error: Throwable) {
KotlinLogger.INSTANCE.logException("Move Nested Member apply failed", error)
try {
pending?.rollback()
} catch (rollbackError: Throwable) {
error.addSuppressed(rollbackError)
KotlinLogger.INSTANCE.logException("Move Nested Member rollback failed", rollbackError)
}
throw error
} finally {
KotlinAnalysisAPISession.invalidate(project)
}
}

/** Restores every transaction participant for NetBeans Undo Last Refactoring. */
Expand Down
Loading
Loading