feat(config): persistent directories, auto-open files, and strict project scope

- Implemented persistent directory memory for Action Model and Dataset generators (per project).
- Added configuration to automatically open generated files in the editor with a customizable limit.
- Enforced strict project-scope file selection for i18n and XSD settings.
- Switched to relative path storage for i18n/XSD configuration to enhance project portability.
- Improved File Browser logic to start at the current directory or fallback to project root.
- Fixed compilation errors and optimized imports in configuration classes.
- Bumped plugin version to 3.2.4.
This commit is contained in:
2026-04-18 11:14:26 +07:00
parent 4da00c10e4
commit 475555da83
17 changed files with 317 additions and 36 deletions

View File

@@ -13,9 +13,11 @@ import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.sdk.dynform.helper.GUtils;
import com.sdk.dynform.tools.config.DynFormSettings;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@@ -30,12 +32,23 @@ public class GenerateBeanAction extends AnAction {
return;
}
DynFormSettings settings = DynFormSettings.getInstance(project);
FileChooserDescriptor descriptor = new FileChooserDescriptor(false,true,false,false,false,false);
PathChooserDialog pathChooser = FileChooserFactory.getInstance().createPathChooser(descriptor, project, null);
VirtualFile baseDir = GUtils.findSourceRoot(project);
VirtualFile initialDir = GUtils.findSourceRoot(project);
if (settings.lastBeanPackagePath != null && !settings.lastBeanPackagePath.isEmpty()) {
VirtualFile lastDir = com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByPath(settings.lastBeanPackagePath);
if (lastDir != null && lastDir.isValid()) {
initialDir = lastDir;
}
}
pathChooser.choose(baseDir, virtualFiles -> {
String packageName = GUtils.getSelectedPackage(project, virtualFiles.getFirst());
pathChooser.choose(initialDir, virtualFiles -> {
VirtualFile selectedDir = virtualFiles.getFirst();
settings.lastBeanPackagePath = selectedDir.getPath();
String packageName = GUtils.getSelectedPackage(project, selectedDir);
ArrayList<DbTable> tables = new ArrayList<>();
for (PsiElement psiElement : psiElements) {

View File

@@ -13,9 +13,11 @@ import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.sdk.dynform.helper.GUtils;
import com.sdk.dynform.tools.config.DynFormSettings;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@@ -30,12 +32,23 @@ public class GenerateBeanActionV3 extends AnAction {
return;
}
DynFormSettings settings = DynFormSettings.getInstance(project);
FileChooserDescriptor descriptor = new FileChooserDescriptor(false,true,false,false,false,false);
PathChooserDialog pathChooser = FileChooserFactory.getInstance().createPathChooser(descriptor, project, null);
VirtualFile baseDir = GUtils.findSourceRoot(project);
VirtualFile initialDir = GUtils.findSourceRoot(project);
if (settings.lastBeanPackagePath != null && !settings.lastBeanPackagePath.isEmpty()) {
VirtualFile lastDir = com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByPath(settings.lastBeanPackagePath);
if (lastDir != null && lastDir.isValid()) {
initialDir = lastDir;
}
}
pathChooser.choose(baseDir, virtualFiles -> {
String packageName = GUtils.getSelectedPackage(project, virtualFiles.getFirst());
pathChooser.choose(initialDir, virtualFiles -> {
VirtualFile selectedDir = virtualFiles.getFirst();
settings.lastBeanPackagePath = selectedDir.getPath();
String packageName = GUtils.getSelectedPackage(project, selectedDir);
ArrayList<DbTable> tables = new ArrayList<>();
for (PsiElement psiElement : psiElements) {

View File

@@ -13,9 +13,11 @@ import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.sdk.dynform.helper.GUtils;
import com.sdk.dynform.tools.config.DynFormSettings;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@@ -30,15 +32,22 @@ public class GenerateDatasetAction extends AnAction {
return;
}
DynFormSettings settings = DynFormSettings.getInstance(project);
FileChooserDescriptor descriptor = new FileChooserDescriptor(false, true, false, false, false, false);
descriptor.setTitle("Select Target Directory for Dataset XML");
PathChooserDialog pathChooser = FileChooserFactory.getInstance().createPathChooser(descriptor, project, null);
//VirtualFile baseDir = GUtils.findSourceRoot(project);
VirtualFile baseDir = project.getBaseDir();
VirtualFile initialDir = project.getBaseDir();
if (settings.lastDatasetFolderPath != null && !settings.lastDatasetFolderPath.isEmpty()) {
VirtualFile lastDir = com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByPath(settings.lastDatasetFolderPath);
if (lastDir != null && lastDir.isValid()) {
initialDir = lastDir;
}
}
pathChooser.choose(baseDir, virtualFiles -> {
pathChooser.choose(initialDir, virtualFiles -> {
VirtualFile targetDir = virtualFiles.getFirst();
settings.lastDatasetFolderPath = targetDir.getPath();
ArrayList<DbTable> tables = new ArrayList<>();
for (PsiElement psiElement : psiElements) {

View File

@@ -5,11 +5,13 @@ import com.intellij.database.model.DasTableKey;
import com.intellij.database.psi.DbTable;
import com.intellij.database.util.DasUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.JBIterable;
import com.sdk.dynform.helper.GUtils;
import com.sdk.dynform.tools.config.DynFormSettings;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
@@ -39,7 +41,7 @@ public class GeneratorServices {
this.version = version;
}
private void genDataModel(Template template, Map<String, Object> model, VirtualFile targetDir, String classFile, ProgressIndicator indicator) {
private VirtualFile genDataModel(Template template, Map<String, Object> model, VirtualFile targetDir, String classFile, ProgressIndicator indicator) {
try {
VirtualFile outputFile = targetDir.findOrCreateChildData(this, classFile);
ApplicationManager.getApplication().runWriteAction(() -> {
@@ -51,11 +53,20 @@ public class GeneratorServices {
indicator.setText2("Generated " + outputFile.getName());
GUtils.showInfo(project, "ActionModels Generator", "Generated " + outputFile.getName());
});
return outputFile;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void openInEditor(VirtualFile file) {
if (file != null) {
ApplicationManager.getApplication().invokeLater(() -> {
FileEditorManager.getInstance(project).openFile(file, true);
});
}
}
public void execute(@NotNull ProgressIndicator indicator) {
// Use AtomicInteger to safely count files within the lambda expression
AtomicInteger fileCount = new AtomicInteger(0);
@@ -81,30 +92,43 @@ public class GeneratorServices {
Template tmpDTO = cfg.getTemplate("actionDTO.ftl");
Template tmpDTOExt = cfg.getTemplate("actionDTO.extend.ftl");
tables.forEach(table -> {
DynFormSettings settings = DynFormSettings.getInstance(project);
for (int i = 0; i < tables.size(); i++) {
DbTable table = tables.get(i);
final boolean shouldOpen = settings.autoOpenGeneratedFiles && i < settings.openFilesLimit;
Map<String, Object> model = createModelForTable(table);
String className = model.get("className").toString();
String classFile = className + ".java";
genDataModel(tmpBean, model, beanDir, classFile, indicator);
VirtualFile fBean = genDataModel(tmpBean, model, beanDir, classFile, indicator);
fileCount.getAndIncrement();
VirtualFile fBeanExt = null;
if (!Files.exists(Path.of(beanExtDir.toNioPath().toString(), classFile))) {
genDataModel(tmpBeanExt, model, beanExtDir, classFile, indicator);
fBeanExt = genDataModel(tmpBeanExt, model, beanExtDir, classFile, indicator);
fileCount.getAndIncrement();
}
// FIX: Use a separate variable for the DTO filename to avoid overwriting the wrong file.
String dtoClassFile = "DTO_" + className + ".java";
genDataModel(tmpDTO, model, DTODir, dtoClassFile, indicator);
VirtualFile fDTO = genDataModel(tmpDTO, model, DTODir, dtoClassFile, indicator);
fileCount.getAndIncrement();
VirtualFile fDTOExt = null;
if (!Files.exists(Path.of(DTOExtDir.toNioPath().toString(), dtoClassFile))) {
genDataModel(tmpDTOExt, model, DTOExtDir, dtoClassFile, indicator);
fDTOExt = genDataModel(tmpDTOExt, model, DTOExtDir, dtoClassFile, indicator);
fileCount.getAndIncrement();
}
});
if (shouldOpen) {
openInEditor(fBean);
openInEditor(fBeanExt);
openInEditor(fDTO);
openInEditor(fDTOExt);
}
}
// After the loop finishes, show a single, helpful summary notification.
String message = String.format("Generated %d files for %d tables successfully.", fileCount.get(), tables.size());
@@ -125,14 +149,22 @@ public class GeneratorServices {
Template tmpDataset = cfg.getTemplate("dataset.ftl");
tables.forEach(table -> {
DynFormSettings settings = DynFormSettings.getInstance(project);
for (int i = 0; i < tables.size(); i++) {
DbTable table = tables.get(i);
final boolean shouldOpen = settings.autoOpenGeneratedFiles && i < settings.openFilesLimit;
Map<String, Object> model = createModelForTable(table);
String tableName = model.get("tableName").toString();
String fileName = GUtils.capitalize(tableName) + ".xml";
genDataModel(tmpDataset, model, targetDir, fileName, indicator);
VirtualFile fDataset = genDataModel(tmpDataset, model, targetDir, fileName, indicator);
fileCount.getAndIncrement();
});
if (shouldOpen) {
openInEditor(fDataset);
}
}
String message = String.format("Generated %d dataset XML files successfully.", fileCount.get());
GUtils.showInfo(project, "Dataset XML Generation Complete", message);

View File

@@ -4,9 +4,12 @@ import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.ui.TextComponentAccessor;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.sdk.dynform.tools.dynform.DynFormXsdScanner;
import org.jetbrains.annotations.NotNull;
@@ -14,6 +17,7 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.io.File;
public class DynFormConfigurable implements Configurable {
@@ -25,6 +29,8 @@ public class DynFormConfigurable implements Configurable {
private TextFieldWithBrowseButton i18nMessageFileField;
private TextFieldWithBrowseButton xsdFolderField;
private JTextField xsdPrefixField;
private JCheckBox autoOpenCheck;
private JSpinner openLimitSpinner;
public DynFormConfigurable(@NotNull Project project) {
this.project = project;
@@ -87,15 +93,38 @@ public class DynFormConfigurable implements Configurable {
i18nGbc.weightx = 1.0;
i18nMessageFileField = new TextFieldWithBrowseButton();
VirtualFile projectDir = ProjectUtil.guessProjectDir(project);
FileChooserDescriptor xmlDescriptor = new FileChooserDescriptor(true, false, false, false, false, false) {
@Override
public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
return super.isFileVisible(file, showHiddenFiles) && (file.isDirectory() || "xml".equalsIgnoreCase(file.getExtension()));
if (!super.isFileVisible(file, showHiddenFiles)) return false;
if (projectDir == null) return true;
return VfsUtil.isAncestor(projectDir, file, false);
}
};
@Override
public void validateSelectedFiles(VirtualFile @NotNull [] files) throws Exception {
super.validateSelectedFiles(files);
for (VirtualFile file : files) {
if (projectDir != null && !VfsUtil.isAncestor(projectDir, file, false)) {
throw new Exception("File must be within the project directory.");
}
}
}
}.withRoots(projectDir);
i18nMessageFileField.addBrowseFolderListener("Select Message XML File", "Select the main message bundle XML file",
project, xmlDescriptor, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
i18nMessageFileField.addBrowseFolderListener("Select Message XML File", "Select the main message bundle XML file (within project)",
project, xmlDescriptor, new TextComponentAccessor<>() {
@Override
public String getText(JTextField textField) {
return resolvePath(textField.getText(), projectDir);
}
@Override
public void setText(JTextField textField, @NotNull String text) {
textField.setText(relativizePath(text, projectDir));
}
});
i18nPanel.add(i18nMessageFileField, i18nGbc);
@@ -128,8 +157,38 @@ public class DynFormConfigurable implements Configurable {
xsdGbc.gridx = 1;
xsdGbc.weightx = 1.0;
xsdFolderField = new TextFieldWithBrowseButton();
xsdFolderField.addBrowseFolderListener("Select XSD Folder", "Select the folder containing DynForm .xsd schemas",
project, FileChooserDescriptorFactory.createSingleFolderDescriptor(), TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
FileChooserDescriptor folderDescriptor = new FileChooserDescriptor(false, true, false, false, false, false) {
@Override
public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
if (!super.isFileVisible(file, showHiddenFiles)) return false;
if (projectDir == null) return true;
return VfsUtil.isAncestor(projectDir, file, false);
}
@Override
public void validateSelectedFiles(VirtualFile @NotNull [] files) throws Exception {
super.validateSelectedFiles(files);
for (VirtualFile file : files) {
if (projectDir != null && !VfsUtil.isAncestor(projectDir, file, false)) {
throw new Exception("Folder must be within the project directory.");
}
}
}
}.withRoots(projectDir);
xsdFolderField.addBrowseFolderListener("Select XSD Folder", "Select the folder containing DynForm .xsd schemas (within project)",
project, folderDescriptor, new TextComponentAccessor<>() {
@Override
public String getText(JTextField textField) {
return resolvePath(textField.getText(), projectDir);
}
@Override
public void setText(JTextField textField, @NotNull String text) {
textField.setText(relativizePath(text, projectDir));
}
});
xsdPanel.add(xsdFolderField, xsdGbc);
xsdGbc.gridx = 0;
@@ -142,6 +201,34 @@ public class DynFormConfigurable implements Configurable {
mainPanel.add(xsdPanel, gbc);
// --- Generator Settings Group ---
gbc.gridy++;
JPanel generatorPanel = new JPanel(new GridBagLayout());
generatorPanel.setBorder(BorderFactory.createTitledBorder("Action Models Generator"));
GridBagConstraints genGbc = new GridBagConstraints();
genGbc.fill = GridBagConstraints.HORIZONTAL;
genGbc.insets = new Insets(2, 2, 2, 2);
genGbc.gridx = 0;
genGbc.gridy = 0;
genGbc.weightx = 1.0;
genGbc.gridwidth = 2;
autoOpenCheck = new JCheckBox("Automatically open generated files in editor");
generatorPanel.add(autoOpenCheck, genGbc);
genGbc.gridy++;
genGbc.gridwidth = 1;
genGbc.weightx = 0.0;
generatorPanel.add(new JLabel("Limit of tables to open:"), genGbc);
genGbc.gridx = 1;
genGbc.weightx = 1.0;
openLimitSpinner = new JSpinner(new SpinnerNumberModel(3, 0, 100, 1));
generatorPanel.add(openLimitSpinner, genGbc);
mainPanel.add(generatorPanel, gbc);
// Spacer
gbc.gridy++;
gbc.weighty = 1.0;
@@ -153,21 +240,30 @@ public class DynFormConfigurable implements Configurable {
@Override
public boolean isModified() {
DynFormSettings settings = DynFormSettings.getInstance(project);
VirtualFile projectDir = ProjectUtil.guessProjectDir(project);
return settings.displayMode != getCurrentModeFromUI() ||
settings.showIcon != showIconCheck.isSelected() ||
!settings.i18nMessageFile.equals(i18nMessageFileField.getText()) ||
!settings.xsdFolderPath.equals(xsdFolderField.getText()) ||
!settings.xsdPrefix.equals(xsdPrefixField.getText());
!settings.i18nMessageFile.equals(relativizePath(i18nMessageFileField.getText(), projectDir)) ||
!settings.xsdFolderPath.equals(relativizePath(xsdFolderField.getText(), projectDir)) ||
!settings.xsdPrefix.equals(xsdPrefixField.getText()) ||
settings.autoOpenGeneratedFiles != autoOpenCheck.isSelected() ||
settings.openFilesLimit != (int) openLimitSpinner.getValue();
}
@Override
public void apply() {
DynFormSettings settings = DynFormSettings.getInstance(project);
VirtualFile projectDir = ProjectUtil.guessProjectDir(project);
settings.displayMode = getCurrentModeFromUI();
settings.showIcon = showIconCheck.isSelected();
settings.i18nMessageFile = i18nMessageFileField.getText();
settings.xsdFolderPath = xsdFolderField.getText();
settings.i18nMessageFile = relativizePath(i18nMessageFileField.getText(), projectDir);
settings.xsdFolderPath = relativizePath(xsdFolderField.getText(), projectDir);
settings.xsdPrefix = xsdPrefixField.getText();
settings.autoOpenGeneratedFiles = autoOpenCheck.isSelected();
settings.openFilesLimit = (int) openLimitSpinner.getValue();
// Register XSDs to IntelliJ for this project
DynFormXsdScanner.scanAndRegister(project, settings.xsdFolderPath, settings.xsdPrefix);
@@ -176,6 +272,45 @@ public class DynFormConfigurable implements Configurable {
com.intellij.codeInsight.daemon.DaemonCodeAnalyzer.getInstance(project).restart();
}
private String relativizePath(String path, VirtualFile projectDir) {
if (projectDir == null || path == null || path.isEmpty()) return path;
File file = new File(path);
if (!file.isAbsolute()) return path; // Already relative or placeholder
VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file);
if (vFile != null) {
String relativePath = VfsUtil.getRelativePath(vFile, projectDir);
if (relativePath != null) return relativePath;
}
return path;
}
private String resolvePath(String path, VirtualFile projectDir) {
if (projectDir == null) return path;
if (path == null || path.isEmpty()) return projectDir.getPath();
VirtualFile vFile;
File file = new File(path);
if (file.isAbsolute()) {
vFile = LocalFileSystem.getInstance().findFileByIoFile(file);
} else {
vFile = projectDir.findFileByRelativePath(path);
}
if (vFile != null && VfsUtil.isAncestor(projectDir, vFile, false)) {
return vFile.getPath();
}
return projectDir.getPath();
}
private boolean isWithinProject(String path, VirtualFile projectDir) {
if (projectDir == null || path == null || path.isEmpty()) return true;
File file = new File(path);
String absolutePath = file.getAbsolutePath();
String projectPath = projectDir.getPath();
return absolutePath.startsWith(projectPath);
}
private DynFormSettings.DisplayMode getCurrentModeFromUI() {
if (foldingBtn.isSelected()) return DynFormSettings.DisplayMode.FOLDING;
if (inlayBtn.isSelected()) return DynFormSettings.DisplayMode.INLAY_HINTS;
@@ -185,6 +320,7 @@ public class DynFormConfigurable implements Configurable {
@Override
public void reset() {
DynFormSettings settings = DynFormSettings.getInstance(project);
switch (settings.displayMode) {
case FOLDING: foldingBtn.setSelected(true); break;
case INLAY_HINTS: inlayBtn.setSelected(true); break;
@@ -194,5 +330,7 @@ public class DynFormConfigurable implements Configurable {
i18nMessageFileField.setText(settings.i18nMessageFile);
xsdFolderField.setText(settings.xsdFolderPath);
xsdPrefixField.setText(settings.xsdPrefix);
autoOpenCheck.setSelected(settings.autoOpenGeneratedFiles);
openLimitSpinner.setValue(settings.openFilesLimit);
}
}

View File

@@ -26,6 +26,12 @@ public class DynFormSettings implements PersistentStateComponent<DynFormSettings
public String xsdFolderPath = "";
public String xsdPrefix = "/dynf";
// Action Models Generator Settings
public boolean autoOpenGeneratedFiles = true;
public int openFilesLimit = 3;
public String lastBeanPackagePath = "";
public String lastDatasetFolderPath = "";
public static DynFormSettings getInstance(Project project) {
return project.getService(DynFormSettings.class);
}

View File

@@ -4,6 +4,7 @@ import com.intellij.javaee.ExternalResourceManagerEx;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
@@ -16,6 +17,17 @@ public class DynFormXsdScanner {
if (project == null || folderPath == null || folderPath.isEmpty()) return;
File dir = new File(folderPath);
if (!dir.exists() || !dir.isDirectory()) {
// Try relative path from project root
VirtualFile projectDir = ProjectUtil.guessProjectDir(project);
if (projectDir != null) {
VirtualFile vDir = projectDir.findFileByRelativePath(folderPath);
if (vDir != null && vDir.isDirectory()) {
dir = new File(vDir.getPath());
}
}
}
if (!dir.exists() || !dir.isDirectory()) return;
File[] xsdFiles = dir.listFiles((d, name) -> name.toLowerCase().endsWith(".xsd"));

View File

@@ -1,6 +1,7 @@
package com.sdk.dynform.tools.i18n;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
@@ -29,10 +30,16 @@ public class I18nUtils {
return result;
}
// Try to find the file using its absolute path
// Try to find the file using its absolute path or relative path from project root
VirtualFile file = LocalFileSystem.getInstance().findFileByPath(filePath);
if (file == null) {
VirtualFile projectDir = ProjectUtil.guessProjectDir(project);
if (projectDir != null) {
file = projectDir.findFileByRelativePath(filePath);
}
}
// If not found by absolute path, fallback to searching by filename in the project
// If not found by path, fallback to searching by filename in the project
if (file == null) {
String filename = new java.io.File(filePath).getName();
Collection<VirtualFile> files = FilenameIndex.getVirtualFilesByName(filename, GlobalSearchScope.everythingScope(project));

View File

@@ -34,6 +34,13 @@
]]></description>
<change-notes><![CDATA[
<h2>[3.2.4]</h2>
<ul>
<li><strong>Persistent Generation Directories:</strong> Generator now remembers the last used directory for Action Beans and Dataset XMLs independently per project.</li>
<li><strong>Auto-Open Generated Files:</strong> Added configuration to automatically open newly created files in the editor, with a customizable limit on the number of files.</li>
<li><strong>Strict Project-Relative Paths:</strong> I18n and XSD configuration now strictly enforces file selection within the project root and stores paths as relative for maximum portability.</li>
<li><strong>Smart File Browser:</strong> Improved file picker logic to automatically start at the current configured directory if it's within the project, falling back to the project root otherwise.</li>
</ul>
<h2>[3.2.3]</h2>
<ul>
<li><strong>Advanced Data Referencing:</strong> Implemented comprehensive reference and completion support for <code>&lt;FOREIGN-DATASETS&gt;</code> and <code>&lt;MASTER-DATA&gt;</code> structures.</li>