diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9179dcd9..19e0ab17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- 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.
- 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.
diff --git a/CoreImpl/pom.xml b/CoreImpl/pom.xml
index 47ef404b..5324e6f1 100644
--- a/CoreImpl/pom.xml
+++ b/CoreImpl/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-core-impl
diff --git a/KotlinCompilerCliBase/pom.xml b/KotlinCompilerCliBase/pom.xml
index 01070bbc..9649602d 100644
--- a/KotlinCompilerCliBase/pom.xml
+++ b/KotlinCompilerCliBase/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-compiler-cli-base
diff --git a/KotlinFixesImpl/pom.xml b/KotlinFixesImpl/pom.xml
index 66817fa4..3c55eb87 100644
--- a/KotlinFixesImpl/pom.xml
+++ b/KotlinFixesImpl/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-fixes-impl
diff --git a/KotlinFormatter/pom.xml b/KotlinFormatter/pom.xml
index 67a8e35c..d4d7e20b 100644
--- a/KotlinFormatter/pom.xml
+++ b/KotlinFormatter/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-formatter
diff --git a/KotlinHighlighting/pom.xml b/KotlinHighlighting/pom.xml
index 5f19ad8c..b98bb91b 100644
--- a/KotlinHighlighting/pom.xml
+++ b/KotlinHighlighting/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-highlighting
diff --git a/KotlinIcons/pom.xml b/KotlinIcons/pom.xml
index 0fa07b8a..4bcefb0b 100644
--- a/KotlinIcons/pom.xml
+++ b/KotlinIcons/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-icons
diff --git a/KotlinIntentionUtils/pom.xml b/KotlinIntentionUtils/pom.xml
index b60e0827..d21133ee 100644
--- a/KotlinIntentionUtils/pom.xml
+++ b/KotlinIntentionUtils/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-intention-utils
diff --git a/KotlinRefactoring/pom.xml b/KotlinRefactoring/pom.xml
index 4ca08293..e8004ee8 100644
--- a/KotlinRefactoring/pom.xml
+++ b/KotlinRefactoring/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-refactoring
diff --git a/Nbm/pom.xml b/Nbm/pom.xml
index a1261aae..e67bdd0d 100644
--- a/Nbm/pom.xml
+++ b/Nbm/pom.xml
@@ -5,7 +5,7 @@
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
../pom.xml
netbeans-plugin-kotlin-nbm
diff --git a/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/gradle/lookup/GradleProjectLookupProviderExtension.java b/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/gradle/lookup/GradleProjectLookupProviderExtension.java
index 0c2079a5..3a2a7c23 100644
--- a/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/gradle/lookup/GradleProjectLookupProviderExtension.java
+++ b/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/gradle/lookup/GradleProjectLookupProviderExtension.java
@@ -34,7 +34,8 @@ public Lookup createAdditionalLookup(Lookup lkp) {
Project project = lkp.lookup(Project.class);
return Lookups.fixed(new KotlinPrivilegedTemplates(),
- new GradleProjectOpenedHook(project));
+ new GradleProjectOpenedHook(project),
+ new io.github.nbplugins.kotlin.nbm.projectsextensions.KotlinProjectSources(project));
}
}
diff --git a/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/j2se/lookup/J2SEProjectLookupProviderExtension.java b/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/j2se/lookup/J2SEProjectLookupProviderExtension.java
index 91a36ca7..ee498597 100644
--- a/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/j2se/lookup/J2SEProjectLookupProviderExtension.java
+++ b/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/j2se/lookup/J2SEProjectLookupProviderExtension.java
@@ -39,8 +39,9 @@ public Lookup createAdditionalLookup(Lookup lkp) {
return Lookups.fixed(
new KotlinPrivilegedTemplates(),
- new J2SEExtendedClassPathProvider(j2seProject),
- new J2SEProjectOpenedHook(j2seProject)
+ new J2SEExtendedClassPathProvider(j2seProject),
+ new J2SEProjectOpenedHook(j2seProject),
+ new io.github.nbplugins.kotlin.nbm.projectsextensions.KotlinProjectSources(j2seProject)
);
}
diff --git a/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/maven/lookup/MavenProjectLookupProviderExtension.java b/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/maven/lookup/MavenProjectLookupProviderExtension.java
index 64122109..26192f17 100644
--- a/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/maven/lookup/MavenProjectLookupProviderExtension.java
+++ b/Nbm/src/main/java/org/jetbrains/kotlin/projectsextensions/maven/lookup/MavenProjectLookupProviderExtension.java
@@ -35,7 +35,8 @@ public Lookup createAdditionalLookup(Lookup lkp) {
Project project = lkp.lookup(Project.class);
return Lookups.fixed(new KotlinPrivilegedTemplates(),
- new MavenProjectOpenedHook(project)
+ new MavenProjectOpenedHook(project),
+ new io.github.nbplugins.kotlin.nbm.projectsextensions.KotlinProjectSources(project)
);
}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/FilteredSourceGroup.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/FilteredSourceGroup.kt
new file mode 100644
index 00000000..c0c07301
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/FilteredSourceGroup.kt
@@ -0,0 +1,59 @@
+/**
+ * ******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import java.beans.PropertyChangeListener
+import javax.swing.Icon
+import org.netbeans.api.project.SourceGroup
+import org.openide.filesystems.FileObject
+
+/** Hides [excludedFolders] from a broader Java source group without changing project metadata. */
+class FilteredSourceGroup(
+ private val delegate: SourceGroup,
+ excludedFolders: Collection
+) : SourceGroup {
+ private val excludedPaths = excludedFolders.map(FileObject::getPath).toSet()
+
+ /** Returns the delegated root folder. */
+ override fun getRootFolder(): FileObject = delegate.rootFolder
+
+ /** Returns the delegated stable source-group identifier. */
+ override fun getName(): String = delegate.name
+
+ /** Returns the delegated Project Explorer label. */
+ override fun getDisplayName(): String = delegate.displayName
+
+ /** Returns the delegated icon. */
+ override fun getIcon(opened: Boolean): Icon? = delegate.getIcon(opened)
+
+ /** Returns whether [file] belongs to the Java group and is not a Kotlin root or its descendant. */
+ override fun contains(file: FileObject): Boolean =
+ delegate.contains(file) && excludedPaths.none { path -> file.path == path || file.path.startsWith("$path/") }
+
+ /** Registers [listener] with the delegated group. */
+ override fun addPropertyChangeListener(listener: PropertyChangeListener?) {
+ delegate.addPropertyChangeListener(listener)
+ }
+
+ /** Removes [listener] from the delegated group. */
+ override fun removePropertyChangeListener(listener: PropertyChangeListener?) {
+ delegate.removePropertyChangeListener(listener)
+ }
+}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectNodeFactories.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectNodeFactories.kt
new file mode 100644
index 00000000..e047427b
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectNodeFactories.kt
@@ -0,0 +1,194 @@
+/**
+ * ******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import javax.swing.event.ChangeEvent
+import javax.swing.event.ChangeListener
+import org.netbeans.api.java.project.JavaProjectConstants
+import org.netbeans.api.project.Project
+import org.netbeans.api.project.ProjectUtils
+import org.netbeans.api.project.SourceGroup
+import org.netbeans.api.project.Sources
+import org.netbeans.spi.java.project.support.ui.PackageView
+import org.netbeans.spi.project.ui.support.NodeFactory
+import org.netbeans.spi.project.ui.support.NodeList
+import org.openide.nodes.Node
+import org.openide.util.ChangeSupport
+import org.openide.util.RequestProcessor
+
+/**
+ * Adds dedicated Kotlin main and test package nodes to project types whose standard
+ * node factory already renders Java source groups.
+ */
+class KotlinProjectNodeFactory : NodeFactory {
+
+ /**
+ * Creates the Kotlin package-node list for [project].
+ *
+ * @param project project being rendered in Project Explorer
+ * @return dynamic list of Kotlin source-root nodes
+ */
+ override fun createNodes(project: Project): NodeList =
+ KotlinSourceNodeList(project, includeJavaGroups = false, includeGradleOtherGroups = false, filterJavaKotlinRoots = false)
+}
+
+/** Supplies the missing source nodes for the legacy third-party Gradle integration. */
+class KotlinLegacyGradleProjectNodeFactory : NodeFactory {
+
+ /**
+ * Creates a source list for the legacy Gradle project type.
+ *
+ * @param project legacy Gradle project being rendered in Project Explorer
+ * @return Kotlin package-tree nodes for the project
+ */
+ override fun createNodes(project: Project): NodeList =
+ KotlinSourceNodeList(project, includeJavaGroups = true, includeGradleOtherGroups = false, filterJavaKotlinRoots = true)
+}
+
+/**
+ * Replaces the built-in Gradle source node factory, which otherwise merges Java and Kotlin trees.
+ */
+class KotlinGradleProjectNodeFactory : NodeFactory {
+
+ /**
+ * Creates a source list that preserves Java, resource, and generated nodes while splitting Kotlin.
+ *
+ * @param project Gradle project being rendered in Project Explorer
+ * @return dynamic list of Gradle source-root nodes
+ */
+ override fun createNodes(project: Project): NodeList =
+ KotlinSourceNodeList(project, includeJavaGroups = true, includeGradleOtherGroups = true, filterJavaKotlinRoots = false)
+}
+
+/** Replaces the J2SE Java node factory so broad Ant roots do not duplicate Kotlin folders. */
+class KotlinJ2SEProjectNodeFactory : NodeFactory {
+
+ /**
+ * Creates a source list with Java package trees filtered to omit conventional Kotlin roots.
+ *
+ * @param project J2SE/Ant project being rendered in Project Explorer
+ * @return dynamic list of separated Java and Kotlin source-root nodes
+ */
+ override fun createNodes(project: Project): NodeList =
+ KotlinSourceNodeList(project, includeJavaGroups = true, includeGradleOtherGroups = false, filterJavaKotlinRoots = true)
+}
+
+/**
+ * Produces PackageView nodes from standard and Kotlin source groups.
+ *
+ * The Gradle implementation uses public `Sources` type names deliberately: the Gradle Java
+ * implementation owns those types but the Kotlin plugin must not take a hard module dependency on it.
+ */
+private class KotlinSourceNodeList(
+ private val project: Project,
+ private val includeJavaGroups: Boolean,
+ private val includeGradleOtherGroups: Boolean,
+ private val filterJavaKotlinRoots: Boolean
+) : NodeList, ChangeListener {
+
+ private val changes = ChangeSupport(this)
+
+ /**
+ * Returns ordered source groups for Project Explorer.
+ *
+ * @return Java roots when requested, followed by separated Kotlin roots and Gradle auxiliary roots
+ */
+ override fun keys(): List {
+ val sources = ProjectUtils.getSources(project)
+ val groups = mutableListOf()
+ if (includeJavaGroups) {
+ val kotlinRoots = KotlinProjectSourceGroups.findRoots(project.projectDirectory).map(KotlinProjectSourceRoot::folder)
+ groups += sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)
+ .map { group -> if (filterJavaKotlinRoots) FilteredSourceGroup(group, kotlinRoots) else group }
+ }
+
+ val projectKotlinGroups = sources.getSourceGroups(KotlinProjectSourceGroups.KOTLIN_SOURCE_TYPE) +
+ sources.getSourceGroups(KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_TYPE)
+ val kotlinGroupPaths = projectKotlinGroups.map { it.rootFolder.path }.toSet()
+ val gradleKotlinGroups = sources.getSourceGroups("kotlin")
+ .filterNot { it.rootFolder.path in kotlinGroupPaths }
+ groups += gradleKotlinGroups.map(::labelKotlinGroup)
+ groups += projectKotlinGroups
+
+ if (includeGradleOtherGroups) {
+ groups += sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_RESOURCES)
+ groups += sources.getSourceGroups("generated")
+ }
+ return groups.distinctBy { it.rootFolder.path }
+ }
+
+ /**
+ * Creates a package-view node for [key].
+ *
+ * @param key source group to render
+ * @return package tree rooted at [key]
+ */
+ override fun node(key: SourceGroup): Node = PackageView.createPackageView(key)
+
+ /** Starts listening for source-model changes. */
+ override fun addNotify() {
+ ProjectUtils.getSources(project).addChangeListener(this)
+ }
+
+ /** Stops listening for source-model changes. */
+ override fun removeNotify() {
+ ProjectUtils.getSources(project).removeChangeListener(this)
+ }
+
+ /**
+ * Schedules an Explorer refresh outside the source-model callback stack.
+ *
+ * @param event source-model change event
+ */
+ override fun stateChanged(event: ChangeEvent) {
+ REFRESH_PROCESSOR.post { changes.fireChange() }
+ }
+
+ /**
+ * Registers a [listener] for changes in source nodes.
+ *
+ * @param listener listener to register
+ */
+ override fun addChangeListener(listener: ChangeListener) {
+ changes.addChangeListener(listener)
+ }
+
+ /**
+ * Removes a [listener] from source-node changes.
+ *
+ * @param listener listener to remove
+ */
+ override fun removeChangeListener(listener: ChangeListener) {
+ changes.removeChangeListener(listener)
+ }
+
+ /** Gives a Gradle Kotlin source group a stable language-specific package-tree label. */
+ private fun labelKotlinGroup(group: SourceGroup): SourceGroup = when {
+ group.rootFolder.path.endsWith("/src/main/kotlin") ->
+ NamedSourceGroup(group, KotlinProjectSourceGroups.KOTLIN_SOURCE_PACKAGES)
+ group.rootFolder.path.endsWith("/src/test/kotlin") ->
+ NamedSourceGroup(group, KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_PACKAGES)
+ else -> group
+ }
+
+ private companion object {
+ val REFRESH_PROCESSOR = RequestProcessor(KotlinSourceNodeList::class.java)
+ }
+}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourceGroups.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourceGroups.kt
new file mode 100644
index 00000000..d80ae78c
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourceGroups.kt
@@ -0,0 +1,107 @@
+/**
+ * ******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import java.beans.PropertyChangeListener
+import javax.swing.Icon
+import org.netbeans.api.project.SourceGroup
+import org.openide.filesystems.FileObject
+
+/** A conventional Kotlin source root and its Project Explorer role. */
+data class KotlinProjectSourceRoot(val folder: FileObject, val test: Boolean) {
+
+ /** The label displayed for this root in Project Explorer. */
+ val displayName: String
+ get() = if (test) KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_PACKAGES
+ else KotlinProjectSourceGroups.KOTLIN_SOURCE_PACKAGES
+}
+
+/**
+ * Locates conventional Kotlin source roots and creates source groups for Project Explorer.
+ *
+ * This deliberately treats only `src/main/kotlin` and `src/test/kotlin` as Kotlin roots.
+ * Arbitrary source directories configured by a build tool remain visible to K2 analysis via
+ * the existing classpath providers, but are not guessed as Kotlin trees in the UI.
+ */
+object KotlinProjectSourceGroups {
+
+ /** Sources type used to query main Kotlin roots from a project's [org.netbeans.api.project.Sources]. */
+ const val KOTLIN_SOURCE_TYPE = "kotlin-main"
+
+ /** Sources type used to query test Kotlin roots from a project's [org.netbeans.api.project.Sources]. */
+ const val KOTLIN_TEST_SOURCE_TYPE = "kotlin-test"
+
+ /** Project Explorer label for Kotlin production packages. */
+ const val KOTLIN_SOURCE_PACKAGES = "Kotlin Source Packages"
+
+ /** Project Explorer label for Kotlin test packages. */
+ const val KOTLIN_TEST_SOURCE_PACKAGES = "Kotlin Test Packages"
+
+ /**
+ * Finds existing conventional Kotlin roots below [projectDirectory].
+ *
+ * @param projectDirectory root directory of a NetBeans project
+ * @return existing main root first and test root second
+ */
+ fun findRoots(projectDirectory: FileObject): List =
+ listOf("src/main/kotlin" to false, "src/test/kotlin" to true)
+ .mapNotNull { (path, test) ->
+ projectDirectory.getFileObject(path)
+ ?.takeIf(FileObject::isFolder)
+ ?.let { KotlinProjectSourceRoot(it, test) }
+ }
+
+ /**
+ * Creates de-duplicated source groups for [roots].
+ *
+ * @param roots Kotlin roots to expose in Project Explorer
+ * @return one source group per distinct root folder
+ */
+ fun createSourceGroups(roots: Collection): Array =
+ roots.distinctBy { it.folder.path }
+ .map(::KotlinProjectSourceGroup)
+ .toTypedArray()
+}
+
+/** Source group wrapper giving a Kotlin root its dedicated Explorer label. */
+private class KotlinProjectSourceGroup(private val root: KotlinProjectSourceRoot) : SourceGroup {
+
+ /** Returns the Kotlin root folder. */
+ override fun getRootFolder(): FileObject = root.folder
+
+ /** Returns a stable source-group identifier. */
+ override fun getName(): String = root.folder.path
+
+ /** Returns the user-visible Kotlin package-tree label. */
+ override fun getDisplayName(): String = root.displayName
+
+ /** Returns the ordinary NetBeans source-root icon. */
+ override fun getIcon(opened: Boolean): Icon? = null
+
+ /** Tests whether [file] belongs to this source root. */
+ override fun contains(file: FileObject): Boolean =
+ file == root.folder || file.path.startsWith("${root.folder.path}/")
+
+ /** This static root has no dynamic properties. */
+ override fun addPropertyChangeListener(listener: PropertyChangeListener?) = Unit
+
+ /** This static root has no dynamic properties. */
+ override fun removePropertyChangeListener(listener: PropertyChangeListener?) = Unit
+}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSources.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSources.kt
new file mode 100644
index 00000000..732fa8d2
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSources.kt
@@ -0,0 +1,76 @@
+/**
+ * ******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import javax.swing.event.ChangeListener
+import org.netbeans.api.project.Project
+import org.netbeans.api.project.SourceGroup
+import org.netbeans.api.project.Sources
+
+/**
+ * Contributes conventional Kotlin roots to a project's merged [Sources] lookup.
+ *
+ * @param project project whose `src/main/kotlin` and `src/test/kotlin` folders are exposed
+ */
+class KotlinProjectSources(private val project: Project) : Sources {
+
+ /** Sources detected during the last query, used to report structural root changes. */
+ private var roots: List = emptyList()
+
+ /** Listeners notified when conventional Kotlin roots appear or disappear. */
+ private val listeners = mutableSetOf()
+
+ /**
+ * Returns Kotlin source groups for the requested [type].
+ *
+ * @param type source-group type requested by a NetBeans client
+ * @return main or test Kotlin roots for the plugin's Kotlin source types, otherwise no roots
+ */
+ override fun getSourceGroups(type: String): Array {
+ val currentRoots = KotlinProjectSourceGroups.findRoots(project.projectDirectory)
+ if (currentRoots != roots) {
+ roots = currentRoots
+ notifyRootChange()
+ }
+ return when (type) {
+ KotlinProjectSourceGroups.KOTLIN_SOURCE_TYPE ->
+ KotlinProjectSourceGroups.createSourceGroups(currentRoots.filterNot(KotlinProjectSourceRoot::test))
+ KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_TYPE ->
+ KotlinProjectSourceGroups.createSourceGroups(currentRoots.filter(KotlinProjectSourceRoot::test))
+ else -> emptyArray()
+ }
+ }
+
+ /** Registers [listener] for changes in the conventional Kotlin root set. */
+ override fun addChangeListener(listener: ChangeListener?) {
+ if (listener != null) listeners += listener
+ }
+
+ /** Removes [listener] from conventional Kotlin root-set notifications. */
+ override fun removeChangeListener(listener: ChangeListener?) {
+ if (listener != null) listeners -= listener
+ }
+
+ /** Notifies Explorer clients after the root set changes. */
+ private fun notifyRootChange() {
+ val event = javax.swing.event.ChangeEvent(this)
+ listeners.toList().forEach { it.stateChanged(event) }
+ }
+}
diff --git a/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/NamedSourceGroup.kt b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/NamedSourceGroup.kt
new file mode 100644
index 00000000..6a409c08
--- /dev/null
+++ b/Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/NamedSourceGroup.kt
@@ -0,0 +1,54 @@
+/**
+ * ******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import java.beans.PropertyChangeListener
+import javax.swing.Icon
+import org.netbeans.api.project.SourceGroup
+import org.openide.filesystems.FileObject
+
+/** Delegates a source group while replacing its Project Explorer display name. */
+class NamedSourceGroup(private val delegate: SourceGroup, private val displayName: String) : SourceGroup {
+
+ /** Returns the delegated root folder. */
+ override fun getRootFolder(): FileObject = delegate.rootFolder
+
+ /** Returns the delegated stable source-group identifier. */
+ override fun getName(): String = delegate.name
+
+ /** Returns the supplied Project Explorer label. */
+ override fun getDisplayName(): String = displayName
+
+ /** Returns the delegated icon. */
+ override fun getIcon(opened: Boolean): Icon? = delegate.getIcon(opened)
+
+ /** Tests whether [file] is in the delegated source group. */
+ override fun contains(file: FileObject): Boolean = delegate.contains(file)
+
+ /** Registers [listener] with the delegated source group. */
+ override fun addPropertyChangeListener(listener: PropertyChangeListener?) {
+ delegate.addPropertyChangeListener(listener)
+ }
+
+ /** Removes [listener] from the delegated source group. */
+ override fun removePropertyChangeListener(listener: PropertyChangeListener?) {
+ delegate.removePropertyChangeListener(listener)
+ }
+}
diff --git a/Nbm/src/main/resources/org/jetbrains/kotlin/layer.xml b/Nbm/src/main/resources/org/jetbrains/kotlin/layer.xml
index 091d756c..b8420384 100644
--- a/Nbm/src/main/resources/org/jetbrains/kotlin/layer.xml
+++ b/Nbm/src/main/resources/org/jetbrains/kotlin/layer.xml
@@ -269,16 +269,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/FilteredSourceGroupTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/FilteredSourceGroupTest.kt
new file mode 100644
index 00000000..a79dbe40
--- /dev/null
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/FilteredSourceGroupTest.kt
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import java.beans.PropertyChangeListener
+import javax.swing.Icon
+import org.netbeans.api.project.SourceGroup
+import org.netbeans.junit.NbTestCase
+import org.openide.filesystems.FileObject
+import org.openide.filesystems.FileUtil
+
+/** Tests filtering Kotlin directories from a broad Ant/J2SE Java source group. */
+class FilteredSourceGroupTest : NbTestCase("FilteredSourceGroupTest") {
+
+ /** Hides a Kotlin subtree while retaining Java packages in the same broad root. */
+ fun testContains_excludesKotlinSubtree() {
+ clearWorkDir()
+ val root = FileUtil.toFileObject(workDir)
+ val javaFile = FileUtil.createData(root, "src/main/java/sample/JavaFile.java")
+ val kotlinFile = FileUtil.createData(root, "src/main/kotlin/sample/KotlinFile.kt")
+ val javaRoot = root.getFileObject("src")!!
+ val kotlinRoot = root.getFileObject("src/main/kotlin")!!
+
+ val filtered = FilteredSourceGroup(TestSourceGroup(javaRoot), listOf(kotlinRoot))
+
+ assertTrue(filtered.contains(javaFile))
+ assertFalse(filtered.contains(kotlinFile))
+ assertFalse(filtered.contains(kotlinRoot))
+ }
+
+ /** Minimal source group used to test filtering independently from a project type. */
+ private class TestSourceGroup(private val root: FileObject) : SourceGroup {
+ override fun getRootFolder(): FileObject = root
+ override fun getName(): String = root.path
+ override fun getDisplayName(): String = "Source Packages"
+ override fun getIcon(opened: Boolean): Icon? = null
+ override fun contains(file: FileObject): Boolean = file == root || file.path.startsWith("${root.path}/")
+ override fun addPropertyChangeListener(listener: PropertyChangeListener?) = Unit
+ override fun removePropertyChangeListener(listener: PropertyChangeListener?) = Unit
+ }
+}
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectNodeFactoryTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectNodeFactoryTest.kt
new file mode 100644
index 00000000..0ed40da8
--- /dev/null
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectNodeFactoryTest.kt
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import java.beans.PropertyChangeListener
+import javax.swing.Icon
+import javax.swing.event.ChangeListener
+import org.netbeans.api.project.Project
+import org.netbeans.api.project.SourceGroup
+import org.netbeans.api.project.Sources
+import org.netbeans.junit.NbTestCase
+import org.openide.filesystems.FileObject
+import org.openide.filesystems.FileUtil
+import org.openide.util.Lookup
+import org.openide.util.lookup.Lookups
+
+/** Tests the source-root order and language separation supplied to Project Explorer node factories. */
+class KotlinProjectNodeFactoryTest : NbTestCase("KotlinProjectNodeFactoryTest") {
+
+ /** Separates Java and Kotlin main/test groups when replacing the Gradle source node factory. */
+ fun testGradleFactory_separatesJavaAndKotlinGroups() {
+ clearWorkDir()
+ val root = FileUtil.toFileObject(workDir)
+ val javaMain = FileUtil.createFolder(root, "src/main/java")
+ val javaTest = FileUtil.createFolder(root, "src/test/java")
+ val kotlinMain = FileUtil.createFolder(root, "src/main/kotlin")
+ val kotlinTest = FileUtil.createFolder(root, "src/test/kotlin")
+ val standardSources = TestSources(
+ mapOf(
+ "java" to arrayOf(TestSourceGroup(javaMain, "Source Packages"), TestSourceGroup(javaTest, "Test Packages")),
+ "kotlin" to arrayOf(TestSourceGroup(kotlinMain, "Source Packages [kotlin]"), TestSourceGroup(kotlinTest, "Test Packages [kotlin]")),
+ KotlinProjectSourceGroups.KOTLIN_SOURCE_TYPE to arrayOf(
+ TestSourceGroup(kotlinMain, KotlinProjectSourceGroups.KOTLIN_SOURCE_PACKAGES)
+ ),
+ KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_TYPE to arrayOf(
+ TestSourceGroup(kotlinTest, KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_PACKAGES)
+ )
+ )
+ )
+ val project = TestProject(root, standardSources)
+
+ val groups = KotlinGradleProjectNodeFactory().createNodes(project).keys()
+
+ assertEquals(
+ listOf(javaMain, javaTest, kotlinMain, kotlinTest),
+ groups.map(SourceGroup::getRootFolder)
+ )
+ assertEquals(
+ listOf(
+ "Source Packages", "Test Packages",
+ KotlinProjectSourceGroups.KOTLIN_SOURCE_PACKAGES,
+ KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_PACKAGES
+ ),
+ groups.map(SourceGroup::getDisplayName)
+ )
+ }
+
+ /** Minimal source provider with preconfigured type-to-group mappings. */
+ private class TestSources(private val groups: Map>) : Sources {
+ override fun getSourceGroups(type: String): Array = groups[type] ?: emptyArray()
+ override fun addChangeListener(listener: ChangeListener?) = Unit
+ override fun removeChangeListener(listener: ChangeListener?) = Unit
+ }
+
+ /** Project whose lookup exposes the test source provider directly. */
+ private class TestProject(private val root: FileObject, sources: Sources) : Project {
+ private val lookup = Lookups.fixed(this, sources)
+ override fun getProjectDirectory(): FileObject = root
+ override fun getLookup(): Lookup = lookup
+ }
+
+ /** Immutable source group for test input. */
+ private class TestSourceGroup(private val root: FileObject, private val label: String) : SourceGroup {
+ override fun getRootFolder(): FileObject = root
+ override fun getName(): String = root.path
+ override fun getDisplayName(): String = label
+ override fun getIcon(opened: Boolean): Icon? = null
+ override fun contains(file: FileObject): Boolean = file == root || file.path.startsWith("${root.path}/")
+ override fun addPropertyChangeListener(listener: PropertyChangeListener?) = Unit
+ override fun removePropertyChangeListener(listener: PropertyChangeListener?) = Unit
+ }
+}
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourceGroupsTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourceGroupsTest.kt
new file mode 100644
index 00000000..27d8f997
--- /dev/null
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourceGroupsTest.kt
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import org.netbeans.junit.NbTestCase
+import org.openide.filesystems.FileObject
+import org.openide.filesystems.FileUtil
+
+/** Tests conventional Kotlin source-root discovery for Project Explorer. */
+class KotlinProjectSourceGroupsTest : NbTestCase("KotlinProjectSourceGroupsTest") {
+
+ /** Creates main and test Kotlin roots with their expected Explorer labels. */
+ fun testFindRoots_discoversConventionalMainAndTestRoots() {
+ val projectDir = projectDirectory()
+ val main = FileUtil.createFolder(projectDir, "src/main/kotlin")
+ val test = FileUtil.createFolder(projectDir, "src/test/kotlin")
+
+ val roots = KotlinProjectSourceGroups.findRoots(projectDir)
+
+ assertEquals(listOf(main, test), roots.map(KotlinProjectSourceRoot::folder))
+ assertEquals(
+ listOf("Kotlin Source Packages", "Kotlin Test Packages"),
+ roots.map(KotlinProjectSourceRoot::displayName)
+ )
+ }
+
+ /** Omits Kotlin roots that do not exist instead of creating empty Explorer nodes. */
+ fun testFindRoots_omitsMissingRoots() {
+ val projectDir = projectDirectory()
+ val main = FileUtil.createFolder(projectDir, "src/main/kotlin")
+
+ val roots = KotlinProjectSourceGroups.findRoots(projectDir)
+
+ assertEquals(listOf(main), roots.map(KotlinProjectSourceRoot::folder))
+ }
+
+ /** Keeps source-root results deterministic even when a folder is supplied more than once. */
+ fun testCreateSourceGroups_deduplicatesRoots() {
+ val projectDir = projectDirectory()
+ val main = FileUtil.createFolder(projectDir, "src/main/kotlin")
+
+ val groups = KotlinProjectSourceGroups.createSourceGroups(
+ listOf(
+ KotlinProjectSourceRoot(main, false),
+ KotlinProjectSourceRoot(main, false)
+ )
+ )
+
+ assertEquals(1, groups.size)
+ assertEquals(main, groups.single().rootFolder)
+ }
+
+ /** Creates an isolated project folder for one test. */
+ private fun projectDirectory(): FileObject {
+ clearWorkDir()
+ return FileUtil.toFileObject(workDir)
+ }
+}
diff --git a/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourcesTest.kt b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourcesTest.kt
new file mode 100644
index 00000000..1c25f903
--- /dev/null
+++ b/Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourcesTest.kt
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Copyright 2000-2016 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.projectsextensions
+
+import org.netbeans.api.project.Project
+import org.netbeans.junit.NbTestCase
+import org.openide.filesystems.FileObject
+import org.openide.filesystems.FileUtil
+import org.openide.util.Lookup
+
+/** Tests the Kotlin source groups contributed to a project's merged Sources lookup. */
+class KotlinProjectSourcesTest : NbTestCase("KotlinProjectSourcesTest") {
+
+ /** Exposes main and test roots only through their respective Kotlin source types. */
+ fun testGetSourceGroups_separatesMainAndTestRoots() {
+ val projectDir = projectDirectory()
+ val main = FileUtil.createFolder(projectDir, "src/main/kotlin")
+ val test = FileUtil.createFolder(projectDir, "src/test/kotlin")
+ val sources = KotlinProjectSources(project(projectDir))
+
+ assertEquals(
+ listOf(main),
+ sources.getSourceGroups(KotlinProjectSourceGroups.KOTLIN_SOURCE_TYPE).map { it.rootFolder }
+ )
+ assertEquals(
+ listOf(test),
+ sources.getSourceGroups(KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_TYPE).map { it.rootFolder }
+ )
+ assertTrue(sources.getSourceGroups("java").isEmpty())
+ }
+
+ /** Uses Kotlin-specific labels instead of leaking the folder name into Project Explorer. */
+ fun testGetSourceGroups_usesKotlinPackageLabels() {
+ val projectDir = projectDirectory()
+ FileUtil.createFolder(projectDir, "src/main/kotlin")
+ FileUtil.createFolder(projectDir, "src/test/kotlin")
+ val sources = KotlinProjectSources(project(projectDir))
+
+ assertEquals(
+ KotlinProjectSourceGroups.KOTLIN_SOURCE_PACKAGES,
+ sources.getSourceGroups(KotlinProjectSourceGroups.KOTLIN_SOURCE_TYPE).single().displayName
+ )
+ assertEquals(
+ KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_PACKAGES,
+ sources.getSourceGroups(KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_TYPE).single().displayName
+ )
+ }
+
+ /** Creates an isolated project directory for one test. */
+ private fun projectDirectory(): FileObject {
+ clearWorkDir()
+ return FileUtil.toFileObject(workDir)
+ }
+
+ /** Returns a minimal project backed by [directory]. */
+ private fun project(directory: FileObject): Project = object : Project {
+ override fun getProjectDirectory(): FileObject = directory
+ override fun getLookup(): Lookup = Lookup.EMPTY
+ }
+}
diff --git a/pom.xml b/pom.xml
index cbc9d211..5e458ecf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.250-SNAPSHOT
+ 0.11.252-SNAPSHOT
pom
Netbeans Plugin Kotlin