From a04d0f5b1c6ac5aa26d279e8a349f2ed5f13c492 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sun, 27 Sep 2026 22:08:49 +0300 Subject: [PATCH] Added separate Kotlin package trees Co-Authored-By: Claude Code --- CHANGELOG.md | 1 + CoreImpl/pom.xml | 2 +- KotlinCompilerCliBase/pom.xml | 2 +- KotlinFixesImpl/pom.xml | 2 +- KotlinFormatter/pom.xml | 2 +- KotlinHighlighting/pom.xml | 2 +- KotlinIcons/pom.xml | 2 +- KotlinIntentionUtils/pom.xml | 2 +- KotlinRefactoring/pom.xml | 2 +- Nbm/pom.xml | 2 +- .../GradleProjectLookupProviderExtension.java | 3 +- .../J2SEProjectLookupProviderExtension.java | 5 +- .../MavenProjectLookupProviderExtension.java | 3 +- .../projectsextensions/FilteredSourceGroup.kt | 59 ++++++ .../KotlinProjectNodeFactories.kt | 194 ++++++++++++++++++ .../KotlinProjectSourceGroups.kt | 107 ++++++++++ .../KotlinProjectSources.kt | 76 +++++++ .../projectsextensions/NamedSourceGroup.kt | 54 +++++ .../resources/org/jetbrains/kotlin/layer.xml | 29 +++ .../FilteredSourceGroupTest.kt | 56 +++++ .../KotlinProjectNodeFactoryTest.kt | 97 +++++++++ .../KotlinProjectSourceGroupsTest.kt | 73 +++++++ .../KotlinProjectSourcesTest.kt | 75 +++++++ pom.xml | 2 +- 24 files changed, 838 insertions(+), 14 deletions(-) create mode 100644 Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/FilteredSourceGroup.kt create mode 100644 Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectNodeFactories.kt create mode 100644 Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourceGroups.kt create mode 100644 Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSources.kt create mode 100644 Nbm/src/main/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/NamedSourceGroup.kt create mode 100644 Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/FilteredSourceGroupTest.kt create mode 100644 Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectNodeFactoryTest.kt create mode 100644 Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourceGroupsTest.kt create mode 100644 Nbm/src/test/kotlin/io/github/nbplugins/kotlin/nbm/projectsextensions/KotlinProjectSourcesTest.kt 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