kotlinSourceGroups(Project project) {
return result;
}
+ /**
+ * Returns source groups available to the Kotlin package wizard.
+ *
+ * NetBeans exposes both ordinary source and test roots through
+ * {@link JavaProjectConstants#SOURCES_TYPE_JAVA}; dedicated Kotlin groups
+ * are appended so the wizard can also target the separated Kotlin trees.
+ *
+ * @param project project whose source roots are offered by the package wizard
+ * @return de-duplicated standard and Kotlin source groups
+ */
+ public static List kotlinPackageSourceGroups(Project project) {
+ Sources sources = ProjectUtils.getSources(project);
+ List kotlinGroups = new ArrayList<>();
+ List result = new ArrayList<>();
+ if (sources != null) {
+ result.addAll(Arrays.asList(sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA)));
+ kotlinGroups.addAll(Arrays.asList(sources.getSourceGroups(KotlinProjectSourceGroups.KOTLIN_SOURCE_TYPE)));
+ kotlinGroups.addAll(Arrays.asList(sources.getSourceGroups(KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_TYPE)));
+ }
+ if (kotlinGroups.isEmpty()) {
+ kotlinGroups.addAll(kotlinSourceGroups(project));
+ }
+ result.addAll(kotlinGroups);
+ Map distinctGroups = new LinkedHashMap<>();
+ for (SourceGroup group : result) {
+ distinctGroups.putIfAbsent(group.getRootFolder().getPath(), group);
+ }
+ return new ArrayList<>(distinctGroups.values());
+ }
+
+ /**
+ * Creates the directory hierarchy that represents a Kotlin package.
+ *
+ * @param root selected Kotlin source root
+ * @param packageName validated dotted package name
+ * @return the package directory
+ * @throws IOException if the folder hierarchy cannot be created
+ */
+ public static FileObject createPackageFolder(FileObject root, String packageName) throws IOException {
+ FileObject folder = root;
+ for (String segment : packageName.split("\\.")) {
+ FileObject child = folder.getFileObject(segment);
+ if (child == null) {
+ folder = folder.createFolder(segment);
+ } else if (child.isFolder()) {
+ folder = child;
+ } else {
+ throw new IOException("Package path is blocked by file: " + child.getPath());
+ }
+ }
+ return folder;
+ }
+
private List> getPanels() {
if (panels == null) {
Project project = Templates.getProject(wizard);
SourceGroup[] groups;
-
- if (project == null) {
+
+ if (targetType == Type.PACKAGE && project != null) {
+ groups = kotlinPackageSourceGroups(project).toArray(new SourceGroup[0]);
+ } else if (project == null) {
List sourceGroups = Lists.newArrayList();
for (Project proj : OpenProjects.getDefault().getOpenProjects()) {
@@ -114,7 +188,8 @@ private List> getPanels() {
sourceGroups.addAll(kotlinSourceGroups(project));
groups = sourceGroups.toArray(new SourceGroup[0]);
}
- packageChooserPanel = PackageChooser.createPackageChooser(project, groups, new KtWizardPanel(), type + " name");
+ packageChooserPanel = PackageChooser.createPackageChooser(
+ project, groups, targetType == Type.PACKAGE ? null : new KtWizardPanel(), targetType, type);
panels = new ArrayList<>();
panels.add(packageChooserPanel);
@@ -160,6 +235,13 @@ public Set> instantiate() throws IOException {
dir == null ? "null" : dir.getPath()
});
+ if (targetType == Type.PACKAGE) {
+ FileObject createdFolder = createPackageFolder(dir, targetName);
+ LOG.log(java.util.logging.Level.INFO,
+ "KtDefaultWizardIterator.instantiate: created package={0}", createdFolder.getPath());
+ return Collections.singleton(DataObject.find(createdFolder));
+ }
+
FileObject createdFile;
try {
String templateBody = template.asText("UTF-8");
diff --git a/Nbm/src/main/java/org/jetbrains/kotlin/file/templates/packagechooser/PackageChooser.java b/Nbm/src/main/java/org/jetbrains/kotlin/file/templates/packagechooser/PackageChooser.java
index 29578a61..48d66fd2 100644
--- a/Nbm/src/main/java/org/jetbrains/kotlin/file/templates/packagechooser/PackageChooser.java
+++ b/Nbm/src/main/java/org/jetbrains/kotlin/file/templates/packagechooser/PackageChooser.java
@@ -28,12 +28,22 @@ public class PackageChooser {
public static String pack = null;
- public static TargetChooserPanel createPackageChooser(Project project, SourceGroup[] folders,
- WizardDescriptor.Panel bottomPanel, String type) {
+ /**
+ * Creates a target chooser for the supplied source groups.
+ *
+ * @param project project that owns the source groups
+ * @param folders source groups offered by the chooser
+ * @param bottomPanel optional panel shown below the target chooser
+ * @param type object type created by the wizard
+ * @param displayName human-readable name of the created object
+ * @return configured target chooser panel
+ */
+ public static TargetChooserPanel createPackageChooser(Project project, SourceGroup[] folders,
+ WizardDescriptor.Panel bottomPanel, Type type, String displayName) {
if (folders.length == 0) {
throw new IllegalArgumentException("No folders selected");
}
- return new TargetChooserPanel(project, folders, bottomPanel, Type.FILE, false, type);
+ return new TargetChooserPanel(project, folders, bottomPanel, type, false, displayName);
}
}
diff --git a/Nbm/src/main/kotlin/org/jetbrains/kotlin/projectsextensions/KotlinPrivilegedTemplates.kt b/Nbm/src/main/kotlin/org/jetbrains/kotlin/projectsextensions/KotlinPrivilegedTemplates.kt
index bd9e559b..a9933790 100644
--- a/Nbm/src/main/kotlin/org/jetbrains/kotlin/projectsextensions/KotlinPrivilegedTemplates.kt
+++ b/Nbm/src/main/kotlin/org/jetbrains/kotlin/projectsextensions/KotlinPrivilegedTemplates.kt
@@ -27,6 +27,7 @@ import org.netbeans.spi.project.ui.PrivilegedTemplates
class KotlinPrivilegedTemplates : PrivilegedTemplates {
override fun getPrivilegedTemplates() = arrayOf(
+ "Templates/Kotlin/Package",
"Templates/Kotlin/class.kt",
"Templates/Kotlin/interface.kt",
"Templates/Kotlin/dataClass.kt",
diff --git a/Nbm/src/main/resources/org/jetbrains/kotlin/Bundle.properties b/Nbm/src/main/resources/org/jetbrains/kotlin/Bundle.properties
index 6bb4ee01..11445147 100644
--- a/Nbm/src/main/resources/org/jetbrains/kotlin/Bundle.properties
+++ b/Nbm/src/main/resources/org/jetbrains/kotlin/Bundle.properties
@@ -17,6 +17,7 @@ string=String
whitespace=Whitespace
annotation=Annotation
Templates/Kotlin=Kotlin
+Templates/Kotlin/Package=Kotlin Package
Templates/Kotlin/class.kt=Kotlin class
Templates/Kotlin/interface.kt=Kotlin interface
Templates/Kotlin/dataClass.kt=Kotlin data class
diff --git a/Nbm/src/main/resources/org/jetbrains/kotlin/layer.xml b/Nbm/src/main/resources/org/jetbrains/kotlin/layer.xml
index f558047f..8bfbc7e4 100644
--- a/Nbm/src/main/resources/org/jetbrains/kotlin/layer.xml
+++ b/Nbm/src/main/resources/org/jetbrains/kotlin/layer.xml
@@ -816,6 +816,14 @@
+
+
+
+
+
+
+
+
diff --git a/Nbm/src/test/java/org/jetbrains/kotlin/file/templates/KtPackageWizardIteratorTest.java b/Nbm/src/test/java/org/jetbrains/kotlin/file/templates/KtPackageWizardIteratorTest.java
new file mode 100644
index 00000000..de568a0a
--- /dev/null
+++ b/Nbm/src/test/java/org/jetbrains/kotlin/file/templates/KtPackageWizardIteratorTest.java
@@ -0,0 +1,216 @@
+/*******************************************************************************
+ * 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 org.jetbrains.kotlin.file.templates;
+
+import io.github.nbplugins.kotlin.nbm.projectsextensions.KotlinProjectSourceGroups;
+import java.beans.PropertyChangeListener;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.swing.Icon;
+import javax.swing.event.ChangeListener;
+import org.jetbrains.kotlin.file.templates.defaultwizard.KtDefaultWizardIterator;
+import org.netbeans.api.java.project.JavaProjectConstants;
+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 Kotlin package-wizard source selection and folder creation. */
+public final class KtPackageWizardIteratorTest extends NbTestCase {
+
+ /** Creates the test fixture. */
+ public KtPackageWizardIteratorTest(String name) {
+ super(name);
+ }
+
+ /** Selects standard and Kotlin source groups, without resources or duplicate roots. */
+ public void testKotlinPackageGroupsIncludeStandardAndKotlinRoots() throws Exception {
+ clearWorkDir();
+ FileObject root = FileUtil.toFileObject(getWorkDir());
+ FileObject javaMain = FileUtil.createFolder(root, "src/main/java");
+ FileObject javaTest = FileUtil.createFolder(root, "src/test/java");
+ FileObject kotlinMain = FileUtil.createFolder(root, "src/main/kotlin");
+ FileObject kotlinTest = FileUtil.createFolder(root, "src/test/kotlin");
+ FileObject resources = FileUtil.createFolder(root, "src/main/resources");
+ TestSources sources = new TestSources();
+ sources.add(JavaProjectConstants.SOURCES_TYPE_JAVA, new TestSourceGroup(javaMain, "Source Packages"));
+ sources.add(JavaProjectConstants.SOURCES_TYPE_JAVA, new TestSourceGroup(javaTest, "Test Packages"));
+ sources.add("resources", new TestSourceGroup(resources, "Resources"));
+ sources.add(KotlinProjectSourceGroups.KOTLIN_SOURCE_TYPE,
+ new TestSourceGroup(kotlinMain, KotlinProjectSourceGroups.KOTLIN_SOURCE_PACKAGES));
+ sources.add(KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_TYPE,
+ new TestSourceGroup(kotlinTest, KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_PACKAGES));
+ sources.add(KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_TYPE,
+ new TestSourceGroup(javaMain, "Duplicate Source Packages"));
+
+ List groups = KtDefaultWizardIterator.kotlinPackageSourceGroups(new TestProject(root, sources));
+
+ assertEquals(List.of(javaMain, javaTest, kotlinMain, kotlinTest),
+ groups.stream().map(SourceGroup::getRootFolder).toList());
+ assertEquals(List.of(
+ "Source Packages",
+ "Test Packages",
+ KotlinProjectSourceGroups.KOTLIN_SOURCE_PACKAGES,
+ KotlinProjectSourceGroups.KOTLIN_TEST_SOURCE_PACKAGES),
+ groups.stream().map(SourceGroup::getDisplayName).toList());
+ }
+
+ /** Keeps standard Source Packages and Test Packages available when no Kotlin root is registered. */
+ public void testKotlinPackageGroupsIncludeJavaOnlyProjectRoots() throws Exception {
+ clearWorkDir();
+ FileObject root = FileUtil.toFileObject(getWorkDir());
+ FileObject javaMain = FileUtil.createFolder(root, "src/main/java");
+ FileObject javaTest = FileUtil.createFolder(root, "src/test/java");
+ TestSources sources = new TestSources();
+ sources.add(JavaProjectConstants.SOURCES_TYPE_JAVA, new TestSourceGroup(javaMain, "Source Packages"));
+ sources.add(JavaProjectConstants.SOURCES_TYPE_JAVA, new TestSourceGroup(javaTest, "Test Packages"));
+
+ List groups = KtDefaultWizardIterator.kotlinPackageSourceGroups(new TestProject(root, sources));
+
+ assertEquals(List.of(javaMain, javaTest), groups.stream().map(SourceGroup::getRootFolder).toList());
+ }
+
+ /** Resolves the manually registered Kotlin Package template to its iterator. */
+ public void testKotlinPackageTemplateRegistersIterator() {
+ FileObject template = FileUtil.getConfigFile("Templates/Kotlin/Package");
+
+ assertNotNull("Kotlin Package template must be registered in the layer", template);
+ assertEquals("Kotlin Package", template.getAttribute("displayName"));
+ assertEquals(Boolean.TRUE, template.getAttribute("requireProject"));
+ assertTrue(template.getAttribute("instantiatingIterator") instanceof KtPackageWizardIterator);
+ }
+
+ /** Creates a nested package below the selected Kotlin root and reuses existing parent folders. */
+ public void testCreateKotlinPackageFolderCreatesNestedFolders() throws Exception {
+ clearWorkDir();
+ FileObject root = FileUtil.toFileObject(getWorkDir());
+ FileObject kotlinRoot = FileUtil.createFolder(root, "src/test/kotlin/com");
+
+ FileObject created = KtDefaultWizardIterator.createPackageFolder(kotlinRoot.getParent(), "com.example.feature");
+
+ assertEquals("src/test/kotlin/com/example/feature", created.getPath().substring(root.getPath().length() + 1));
+ assertTrue(created.isFolder());
+ assertNotNull(kotlinRoot.getParent().getFileObject("com/example/feature"));
+ }
+
+ /** Minimal test project exposing one source provider. */
+ private static final class TestProject implements Project {
+ private final FileObject directory;
+ private final Lookup lookup;
+
+ /** Creates a project rooted at {@code directory}. */
+ TestProject(FileObject directory, Sources sources) {
+ this.directory = directory;
+ this.lookup = Lookups.fixed(this, sources);
+ }
+
+ /** Returns the project directory. */
+ @Override
+ public FileObject getProjectDirectory() {
+ return directory;
+ }
+
+ /** Returns the project lookup. */
+ @Override
+ public Lookup getLookup() {
+ return lookup;
+ }
+ }
+
+ /** Simple map-backed source provider. */
+ private static final class TestSources implements Sources {
+ private final Map> groups = new LinkedHashMap<>();
+
+ /** Adds {@code group} to source groups registered for {@code type}. */
+ void add(String type, SourceGroup group) {
+ groups.computeIfAbsent(type, ignored -> new java.util.ArrayList<>()).add(group);
+ }
+
+ /** Returns the configured source groups. */
+ @Override
+ public SourceGroup[] getSourceGroups(String type) {
+ return groups.getOrDefault(type, List.of()).toArray(new SourceGroup[0]);
+ }
+
+ /** This immutable provider has no change notifications. */
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ }
+
+ /** This immutable provider has no change notifications. */
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ }
+ }
+
+ /** Minimal immutable source group. */
+ private static final class TestSourceGroup implements SourceGroup {
+ private final FileObject root;
+ private final String label;
+
+ /** Creates a source group for {@code root}. */
+ TestSourceGroup(FileObject root, String label) {
+ this.root = root;
+ this.label = label;
+ }
+
+ /** Returns the group root. */
+ @Override
+ public FileObject getRootFolder() {
+ return root;
+ }
+
+ /** Returns a stable group name. */
+ @Override
+ public String getName() {
+ return root.getPath();
+ }
+
+ /** Returns the display label. */
+ @Override
+ public String getDisplayName() {
+ return label;
+ }
+
+ /** This test group has no icon. */
+ @Override
+ public Icon getIcon(boolean opened) {
+ return null;
+ }
+
+ /** Tests whether {@code file} is under this group root. */
+ @Override
+ public boolean contains(FileObject file) {
+ return file.equals(root) || file.getPath().startsWith(root.getPath() + "/");
+ }
+
+ /** This immutable group has no change notifications. */
+ @Override
+ public void addPropertyChangeListener(PropertyChangeListener listener) {
+ }
+
+ /** This immutable group has no change notifications. */
+ @Override
+ public void removePropertyChangeListener(PropertyChangeListener listener) {
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 5954b533..10c838a2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
io.github.nbplugins
netbeans-plugin-kotlin
- 0.11.280-SNAPSHOT
+ 0.11.282-SNAPSHOT
pom
Netbeans Plugin Kotlin