refactor: rebrand to DynamicFormTools and add i18n support

- Rebranded plugin from "ActionModelsGenerator" to "Dynamic Form Helper".
- Refactored package structure from "com.sdk.generators" to "com.sdk.dynform.tools".
- Added comprehensive I18n support for Java, XML, and JavaScript:
    - Inlay hints and code folding for internationalization keys.
    - Completion and reference contributors for "message.xml" keys.
    - Configuration settings and UI for i18n tools.
- Introduced support for the ".frml" (DynForm) file type.
- Added specialized DynForm completion and path resolution helpers.
- Updated "build.gradle.kts" with JSP and JavaScript platform dependencies.
- Updated documentation and project metadata to reflect the new name.
This commit is contained in:
2026-04-07 22:39:20 +07:00
parent 351e299d7e
commit 186c729ece
39 changed files with 1759 additions and 36 deletions

View File

@@ -0,0 +1,154 @@
package com.sdk.dynform.helper;
import com.intellij.database.model.DasColumn;
import com.intellij.database.model.DasObject;
import com.intellij.database.model.ObjectKind;
import com.intellij.database.psi.DbObject;
import com.intellij.database.psi.DbTable;
import com.intellij.database.util.DasUtil;
import com.intellij.notification.NotificationGroupManager;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.PackageIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class GUtils {
private static final LinkedHashMap<String, String> TYPE_MAPPING = new LinkedHashMap<>();
static {
TYPE_MAPPING.put("bigint", "NUMBER");
TYPE_MAPPING.put("bit", "STRING");
TYPE_MAPPING.put("boolean", "STRING");
TYPE_MAPPING.put("date", "DATE");
TYPE_MAPPING.put("decimal", "NUMBER");
TYPE_MAPPING.put("double", "NUMBER");
TYPE_MAPPING.put("float", "NUMBER");
TYPE_MAPPING.put("integer", "NUMBER");
TYPE_MAPPING.put("numeric", "NUMBER");
TYPE_MAPPING.put("number", "NUMBER");
TYPE_MAPPING.put("smallint", "NUMBER");
TYPE_MAPPING.put("timestamp", "DATE");
TYPE_MAPPING.put("text", "STRING");
TYPE_MAPPING.put("time", "DATE");
TYPE_MAPPING.put("tinyint", "NUMBER");
TYPE_MAPPING.put("varchar2", "STRING");
TYPE_MAPPING.put("varchar", "STRING");
TYPE_MAPPING.put("char", "STRING");
}
private static final Map<String, String> userDefType = new HashMap<>();
private static String getRawType(String columnDefinition) {
String columnScript = columnDefinition.replaceAll("\s","#");
for (String rawType : TYPE_MAPPING.keySet()) {
if (columnScript.contains("#"+rawType)) {
return rawType;
}
if (columnScript.contains("#"+rawType+"(")) {
return rawType;
}
if (columnScript.contains("#"+rawType+"#(")) {
return rawType;
}
}
return "varchar";
}
public static void createUserDefineType(DbTable table) {
String schemaName = table.getParent() != null ? table.getParent().getName() : "";
DasObject objCatalog = DasUtil.getCatalogObject(table);
if (objCatalog != null) {
objCatalog.getDasChildren(ObjectKind.SCHEMA).forEach(schema -> {
if (schema.getName().equals("public") || schema.getName().equals(schemaName)) {
schema.getDasChildren(ObjectKind.OBJECT_TYPE).forEach(domain -> {
String domainName = domain.getName();
String domainScript = ((DbObject) domain).getText();
userDefType.put(domainName, getRawType(domainScript));
});
}
});
}
}
public static String getFieldType(DasColumn column) {
String columnType = column.getDasType().toDataType().typeName;
String fieldType = TYPE_MAPPING.get(columnType.toLowerCase());
if (fieldType == null) {
fieldType = TYPE_MAPPING.getOrDefault(userDefType.getOrDefault(columnType, "varchar"), "Object");
}
return fieldType;
}
public static String toCamelCase(String s, boolean capitalizeFirst) {
String[] parts = s.split("_");
StringBuilder camelCaseString = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
if (i == 0 && !capitalizeFirst) {
camelCaseString.append(part.toLowerCase());
} else {
camelCaseString.append(capitalize(part));
}
}
return camelCaseString.toString();
}
public static String capitalize(String s) {
if (s == null || s.isEmpty()) {
return s;
}
return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
}
public static String getSelectedPackage(Project project, VirtualFile selectDirectory) {
if (selectDirectory == null) return "";
return PackageIndex.getInstance(project).getPackageNameByDirectory(selectDirectory);
}
public static VirtualFile findSourceRoot(Project project) {
for (VirtualFile root : ProjectRootManager.getInstance(project).getContentSourceRoots()) {
if (root.isDirectory() && !root.getName().equals("resources")) {
return root;
}
}
return null;
}
public static VirtualFile createPackageDirs(Object requestor, VirtualFile sourceRoot, String path) throws IOException {
VirtualFile current = sourceRoot;
for (String part : path.split("/")) {
VirtualFile child = current.findChild(part);
if (child == null) {
child = current.createChildDirectory(requestor, part);
}
current = child;
}
return current;
}
public static void showError(Project project, String message) {
ApplicationManager.getApplication().invokeLater(() -> Messages.showErrorDialog(project, message, "Generation Failed"));
NotificationGroupManager.getInstance()
// This ID should be registered in your plugin.xml
.getNotificationGroup("Dynamic-Form-Tools-Notification")
.createNotification("ActionModels generator", message, NotificationType.ERROR)
.notify(project);
}
public static void showInfo(Project project, String title, String content) {
NotificationGroupManager.getInstance()
// This ID should be registered in your plugin.xml
.getNotificationGroup("Dynamic-Form-Tools-Notification")
.createNotification(title, content, NotificationType.INFORMATION)
.notify(project);
}
}

View File

@@ -0,0 +1,152 @@
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.model.DasColumn;
import com.intellij.database.model.ObjectKind;
import com.intellij.database.psi.DbObject;
import com.intellij.database.psi.DbTable;
import com.intellij.database.util.DasUtil;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileChooser.FileChooserFactory;
import com.intellij.openapi.fileChooser.FileSaverDescriptor;
import com.intellij.openapi.fileChooser.FileSaverDialog;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileWrapper;
import com.intellij.psi.PsiElement;
import com.intellij.util.containers.JBIterable;
import com.sdk.dynform.helper.GUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class GenerateAction extends AnAction {
private static final LinkedHashMap<String, String> TYPE_MAPPING = new LinkedHashMap<>();
static {
TYPE_MAPPING.put("bigint", "Long");
TYPE_MAPPING.put("bit", "Boolean");
TYPE_MAPPING.put("boolean", "Boolean");
TYPE_MAPPING.put("date", "java.util.Date");
TYPE_MAPPING.put("decimal", "java.math.BigDecimal");
TYPE_MAPPING.put("double", "Double");
TYPE_MAPPING.put("float", "Float");
TYPE_MAPPING.put("integer", "Integer");
TYPE_MAPPING.put("numeric", "java.math.BigDecimal");
TYPE_MAPPING.put("smallint", "Short");
TYPE_MAPPING.put("timestamp", "java.util.Date");
TYPE_MAPPING.put("text", "String");
TYPE_MAPPING.put("time", "java.util.Date");
TYPE_MAPPING.put("tinyint", "Byte");
TYPE_MAPPING.put("varchar2", "String");
TYPE_MAPPING.put("varchar", "String");
TYPE_MAPPING.put("char", "String");
}
private static final Map<String,String> userDefType = new HashMap<>();
private static String getRawType(String columnDefinition ) {
for (String rawType : TYPE_MAPPING.keySet()) {
if (columnDefinition.contains(rawType)) {
return rawType;
}
}
return "varchar";
}
private static void createUserDefineType(DbTable table) {
if (userDefType.isEmpty()) {
String schemaName = table.getParent() != null ? table.getParent().getName() : "";
DasUtil.getCatalogObject(table).getDasChildren(ObjectKind.SCHEMA).forEach(schema -> {
if (schema.getName().equals("public") || schema.getName().equals(schemaName)) {
schema.getDasChildren(ObjectKind.OBJECT_TYPE).forEach(domain -> {
String domainName = domain.getName();
String domainScript = ((DbObject) domain).getText();
userDefType.put(domainName, getRawType(domainScript));
});
}
});
}
}
public static String getFieldType(String columnType) {
columnType = columnType.toLowerCase();
String fieldType = TYPE_MAPPING.get(columnType );
if (fieldType == null) {
fieldType = TYPE_MAPPING.getOrDefault(userDefType.getOrDefault(fieldType,"varchar"),"Object" );
}
return fieldType;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Project project = e.getProject();
PsiElement[] psiElements = e.getData(LangDataKeys.PSI_ELEMENT_ARRAY);
if (project == null || psiElements == null || psiElements.length == 0) {
return;
}
for (PsiElement psiElement : psiElements) {
if (psiElement instanceof DbTable) {
generate((DbTable) psiElement, project);
}
}
}
private void generate(DbTable table, Project project) {
if (userDefType.isEmpty()) {
createUserDefineType(table);
}
String className = GUtils.toCamelCase(table.getName(), true);
StringBuilder builder = new StringBuilder();
builder.append("public class ").append(className).append(" {\n\n");
JBIterable<? extends DasColumn> columns = DasUtil.getColumns(table);
for (DasColumn column : columns) {
String fieldName = GUtils.toCamelCase(column.getName(), false);
String fieldType = getFieldType(column.getDasType().toDataType().typeName);
builder.append(" private ").append(fieldType).append(" ").append(fieldName).append(";\n");
}
builder.append("\n");
for (DasColumn column : columns) {
String fieldName = GUtils.toCamelCase(column.getName(), false);
String fieldType = getFieldType(column.getDasType().toDataType().typeName);
String capitalizedFieldName = GUtils.capitalize(fieldName);
builder.append(" public ").append(fieldType).append(" get").append(capitalizedFieldName).append("() {\n");
builder.append(" return ").append(fieldName).append(";\n");
builder.append(" }\n\n");
builder.append(" public void set").append(capitalizedFieldName).append("(").append(fieldType).append(" ").append(fieldName).append(") {\n");
builder.append(" this.").append(fieldName).append(" = ").append(fieldName).append(";\n");
builder.append(" }\n\n");
}
builder.append("}");
FileSaverDescriptor descriptor = new FileSaverDescriptor("Save JavaBean", "Save the generated JavaBean to a file", "java");
FileSaverDialog saveFileDialog = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, project);
VirtualFileWrapper fileWrapper = saveFileDialog.save((VirtualFile) null, className + ".java");
VirtualFile virtualFile = fileWrapper != null ? fileWrapper.getVirtualFile() : null;
if (virtualFile != null) {
ApplicationManager.getApplication().runWriteAction(() -> {
try (OutputStream outputStream = virtualFile.getOutputStream(this)) {
outputStream.write(builder.toString().getBytes(StandardCharsets.UTF_8));
} catch (IOException ex) {
Messages.showErrorDialog(project, "Error saving file: " + ex.getMessage(), "Error");
}
});
}
}
}

View File

@@ -0,0 +1,66 @@
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.psi.DbTable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserFactory;
import com.intellij.openapi.fileChooser.PathChooserDialog;
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.VirtualFile;
import com.intellij.psi.PsiElement;
import com.sdk.dynform.helper.GUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
public class GenerateBeanAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Project project = e.getProject();
PsiElement[] psiElements = e.getData(LangDataKeys.PSI_ELEMENT_ARRAY);
if (project == null || psiElements == null || psiElements.length == 0) {
return;
}
FileChooserDescriptor descriptor = new FileChooserDescriptor(false,true,false,false,false,false);
PathChooserDialog pathChooser = FileChooserFactory.getInstance().createPathChooser(descriptor, project, null);
VirtualFile baseDir = GUtils.findSourceRoot(project);
pathChooser.choose(baseDir, virtualFiles -> {
String packageName = GUtils.getSelectedPackage(project, virtualFiles.getFirst());
ArrayList<DbTable> tables = new ArrayList<>();
for (PsiElement psiElement : psiElements) {
if (psiElement instanceof DbTable) {
tables.add((DbTable) psiElement);
}
}
runGenerator(project, tables, packageName);
});
}
private void runGenerator(Project project,ArrayList<DbTable> tables, String packageName) {
ProgressManager.getInstance().run(new Task.Backgroundable(project, "Generate database action models ...") {
@Override
public void run(@NotNull ProgressIndicator indicator) {
ApplicationManager.getApplication().invokeLater(() ->
WriteCommandAction.runWriteCommandAction(project, () -> {
try {
new GeneratorServices(project,tables,packageName, GeneratorServices.Version.V2).execute(indicator);
} catch (Exception ex) {
GUtils.showError(project, "An error occurred during code generation: " + ex.getMessage());
}
})
);
}
});
}
}

View File

@@ -0,0 +1,66 @@
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.psi.DbTable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserFactory;
import com.intellij.openapi.fileChooser.PathChooserDialog;
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.VirtualFile;
import com.intellij.psi.PsiElement;
import com.sdk.dynform.helper.GUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
public class GenerateBeanActionV3 extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Project project = e.getProject();
PsiElement[] psiElements = e.getData(LangDataKeys.PSI_ELEMENT_ARRAY);
if (project == null || psiElements == null || psiElements.length == 0) {
return;
}
FileChooserDescriptor descriptor = new FileChooserDescriptor(false,true,false,false,false,false);
PathChooserDialog pathChooser = FileChooserFactory.getInstance().createPathChooser(descriptor, project, null);
VirtualFile baseDir = GUtils.findSourceRoot(project);
pathChooser.choose(baseDir, virtualFiles -> {
String packageName = GUtils.getSelectedPackage(project, virtualFiles.getFirst());
ArrayList<DbTable> tables = new ArrayList<>();
for (PsiElement psiElement : psiElements) {
if (psiElement instanceof DbTable) {
tables.add((DbTable) psiElement);
}
}
runGenerator(project, tables, packageName,"V3");
});
}
private void runGenerator(Project project,ArrayList<DbTable> tables, String packageName, String version) {
ProgressManager.getInstance().run(new Task.Backgroundable(project, "Generate database action models ...") {
@Override
public void run(@NotNull ProgressIndicator indicator) {
ApplicationManager.getApplication().invokeLater(() ->
WriteCommandAction.runWriteCommandAction(project, () -> {
try {
new GeneratorServices(project,tables,packageName, GeneratorServices.Version.V3).execute(indicator);
} catch (Exception ex) {
GUtils.showError(project, "An error occurred during code generation: " + ex.getMessage());
}
})
);
}
});
}
}

View File

@@ -0,0 +1,69 @@
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.psi.DbTable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserFactory;
import com.intellij.openapi.fileChooser.PathChooserDialog;
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.VirtualFile;
import com.intellij.psi.PsiElement;
import com.sdk.dynform.helper.GUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
public class GenerateDatasetAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Project project = e.getProject();
PsiElement[] psiElements = e.getData(LangDataKeys.PSI_ELEMENT_ARRAY);
if (project == null || psiElements == null || psiElements.length == 0) {
return;
}
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();
pathChooser.choose(baseDir, virtualFiles -> {
VirtualFile targetDir = virtualFiles.getFirst();
ArrayList<DbTable> tables = new ArrayList<>();
for (PsiElement psiElement : psiElements) {
if (psiElement instanceof DbTable) {
tables.add((DbTable) psiElement);
}
}
runGenerator(project, tables, targetDir);
});
}
private void runGenerator(Project project, ArrayList<DbTable> tables, VirtualFile targetDir) {
ProgressManager.getInstance().run(new Task.Backgroundable(project, "Generate Dataset XML ...") {
@Override
public void run(@NotNull ProgressIndicator indicator) {
ApplicationManager.getApplication().invokeLater(() ->
WriteCommandAction.runWriteCommandAction(project, () -> {
try {
new GeneratorServices(project, tables, "", GeneratorServices.Version.V2).executeDataset(targetDir, indicator);
} catch (Exception ex) {
GUtils.showError(project, "An error occurred during code generation: " + ex.getMessage());
}
})
);
}
});
}
}

View File

@@ -0,0 +1,225 @@
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.model.DasColumn;
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.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 freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import org.jetbrains.annotations.NotNull;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class GeneratorServices {
private final String basePackage;
private final Project project;
private final ArrayList<DbTable> tables;
private final Version version;
public enum Version {V2,V3}
public GeneratorServices(Project project, ArrayList<DbTable> tables, String basePackage, Version version) {
this.tables = tables;
this.project = project;
this.basePackage = basePackage;
this.version = version;
}
private void genDataModel(Template template, Map<String, Object> model, VirtualFile targetDir, String classFile, ProgressIndicator indicator) {
try {
VirtualFile outputFile = targetDir.findOrCreateChildData(this, classFile);
ApplicationManager.getApplication().runWriteAction(() -> {
try (Writer writer = new OutputStreamWriter(outputFile.getOutputStream(this))) {
template.process(model, writer);
} catch (Exception e) {
throw new RuntimeException(e);
}
indicator.setText2("Generated " + outputFile.getName());
GUtils.showInfo(project, "ActionModels Generator", "Generated " + outputFile.getName());
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void execute(@NotNull ProgressIndicator indicator) {
// Use AtomicInteger to safely count files within the lambda expression
AtomicInteger fileCount = new AtomicInteger(0);
try {
VirtualFile sourceRoot = GUtils.findSourceRoot(project);
String packagePath = basePackage.replace('.', '/');
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setClassForTemplateLoading(this.getClass(), "/templates");
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
VirtualFile beanExtDir = GUtils.createPackageDirs(this, sourceRoot, Path.of(packagePath, "/bean").toString());
VirtualFile beanDir = GUtils.createPackageDirs(this, sourceRoot, Path.of(packagePath, "/bean/base").toString());
Template tmpBean = cfg.getTemplate("actionBean.ftl");
Template tmpBeanExt = cfg.getTemplate("actionBean.extend.ftl");
VirtualFile DTOExtDir = GUtils.createPackageDirs(this, sourceRoot, Path.of(packagePath, "/dto").toString());
VirtualFile DTODir = GUtils.createPackageDirs(this, sourceRoot, Path.of(packagePath, "/dto/base").toString());
Template tmpDTO = cfg.getTemplate("actionDTO.ftl");
Template tmpDTOExt = cfg.getTemplate("actionDTO.extend.ftl");
tables.forEach(table -> {
Map<String, Object> model = createModelForTable(table);
String className = model.get("className").toString();
String classFile = className + ".java";
genDataModel(tmpBean, model, beanDir, classFile, indicator);
fileCount.getAndIncrement();
if (!Files.exists(Path.of(beanExtDir.toNioPath().toString(), classFile))) {
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);
fileCount.getAndIncrement();
if (!Files.exists(Path.of(DTOExtDir.toNioPath().toString(), dtoClassFile))) {
genDataModel(tmpDTOExt, model, DTOExtDir, dtoClassFile, indicator);
fileCount.getAndIncrement();
}
});
// 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());
GUtils.showInfo(project, "ActionModels Generation Complete", message);
} catch (Exception ex) {
GUtils.showError(project, "Generation Failed \n" + ex.getMessage());
}
}
public void executeDataset(VirtualFile targetDir, @NotNull ProgressIndicator indicator) {
AtomicInteger fileCount = new AtomicInteger(0);
try {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setClassForTemplateLoading(this.getClass(), "/templates");
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
Template tmpDataset = cfg.getTemplate("dataset.ftl");
tables.forEach(table -> {
Map<String, Object> model = createModelForTable(table);
String tableName = model.get("tableName").toString();
String fileName = GUtils.capitalize(tableName) + ".xml";
genDataModel(tmpDataset, model, targetDir, fileName, indicator);
fileCount.getAndIncrement();
});
String message = String.format("Generated %d dataset XML files successfully.", fileCount.get());
GUtils.showInfo(project, "Dataset XML Generation Complete", message);
} catch (Exception ex) {
GUtils.showError(project, "Generation Failed \n" + ex.getMessage());
}
}
private Map<String, Object> createModelForTable(DbTable table) {
String tableName = table.getName().toUpperCase();
String dbSchema = table.getParent() != null ? table.getParent().getName() : "";
GUtils.createUserDefineType(table);
Map<String, Object> model = new HashMap<>();
model.put("basePackage", basePackage);
model.put("tableName", tableName);
model.put("className", tableName);
model.put("dbSchema", dbSchema);
if (version == Version.V2) {
model.put("db_connector", "sdk.dbutils.*");
model.put("db_dataset", "sdk.dbutils.*");
model.put("db_dto", "sdk.dbutils.*");
} else {
model.put("db_connector", "sdk.db.connector.*");
model.put("db_dataset", "sdk.db.dataset.*");
model.put("db_dto", "sdk.db.dto.*");
}
List<Map<String, Object>> columns = new ArrayList<>();
Set<String> primaryKeys = new HashSet<>();
JBIterable<? extends DasTableKey> dasKeys = DasUtil.getTableKeys(table);
dasKeys.forEach(key -> {
if (key.isPrimary()) {
key.getColumnsRef().names().forEach(column -> primaryKeys.add(column.toUpperCase()));
}
});
String entityName = tableName.toLowerCase();
if (entityName.endsWith("_m")) {
entityName = entityName.substring(0, entityName.length() - 2);
} else if (entityName.endsWith("s")) {
entityName = entityName.substring(0, entityName.length() - 1);
}
final String finalEntityName = entityName;
JBIterable<? extends DasColumn> dasColumns = DasUtil.getColumns(table);
dasColumns.forEach(column -> {
Map<String, Object> colModel = new HashMap<>();
String colName = column.getName().toUpperCase();
String dataType = GUtils.getFieldType(column);
colModel.put("name", colName);
colModel.put("isPk", primaryKeys.contains(colName));
colModel.put("customType", dataType);
// XML Specific fields
String xmlType = "TEXT";
if ("NUMBER".equals(dataType)) {
xmlType = "NUMBER";
} else if ("DATE".equals(dataType)) {
xmlType = "DATE";
}
colModel.put("xmlType", xmlType);
int width = column.getDasType().toDataType().getLength();
colModel.put("width", Math.max(width, 0));
// Label generation: Use field comment if available, otherwise fallback to generated key
String comment = column.getComment();
if (comment != null && !comment.isEmpty()) {
colModel.put("label", comment);
} else {
String fieldPart = colName.toLowerCase();
colModel.put("label", fieldPart);
}
columns.add(colModel);
});
model.put("columns", columns);
model.put("fieldList", columns.stream().map(c -> (String) c.get("name")).collect(Collectors.joining(",")));
model.put("keyList", String.join(",", primaryKeys));
return model;
}
}

View File

@@ -0,0 +1,76 @@
package com.sdk.dynform.tools.helper;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class DynFormCompletionContributor extends CompletionContributor {
public DynFormCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().withParent(PsiLiteralExpression.class),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
PsiLiteralExpression literal = PsiTreeUtil.getParentOfType(position, PsiLiteralExpression.class);
if (literal == null) return;
PsiNewExpression newExp = getNewDynFormExpression(literal);
if (newExp != null) {
PsiExpressionList argList = newExp.getArgumentList();
if (argList != null) {
PsiExpression[] args = argList.getExpressions();
// Suggest modules at index 3
if (args.length >= 4 && args[3] == literal) {
List<String> modules = DynFormPathUtils.getAllModules(position.getProject());
for (String module : modules) {
resultSet.addElement(LookupElementBuilder.create(module)
.withIcon(com.intellij.icons.AllIcons.Nodes.Module));
}
}
// Suggest frml files at index 4
if (args.length >= 5 && args[4] == literal) {
String moduleName = getArgumentValue(args, 3);
if (moduleName != null) {
List<String> frmls = DynFormPathUtils.getFrmlFiles(position.getProject(), moduleName);
for (String frml : frmls) {
resultSet.addElement(LookupElementBuilder.create(frml)
.withIcon(com.intellij.icons.AllIcons.FileTypes.Xml));
}
}
}
}
}
}
});
}
private PsiNewExpression getNewDynFormExpression(PsiLiteralExpression literal) {
PsiElement parent = literal.getParent();
if (parent instanceof PsiExpressionList) {
PsiElement grandParent = parent.getParent();
if (grandParent instanceof PsiNewExpression newExp) {
PsiJavaCodeReferenceElement classRef = newExp.getClassReference();
if (classRef != null && "DynForm".equals(classRef.getReferenceName())) {
return newExp;
}
}
}
return null;
}
private String getArgumentValue(PsiExpression[] args, int index) {
if (args.length > index && args[index] instanceof PsiLiteralExpression literal) {
Object val = literal.getValue();
return val instanceof String ? (String) val : null;
}
return null;
}
}

View File

@@ -0,0 +1,83 @@
package com.sdk.dynform.tools.helper;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class DynFormPathUtils {
public static final String MODULE_BASE_PATH = "src/main/webapp/WEB-INF/app/module";
@Nullable
public static VirtualFile getModuleBaseDir(@NotNull Project project) {
// ค้นหาจาก project root โดยตรง
VirtualFile baseDir = project.getBaseDir();
if (baseDir == null) return null;
return baseDir.findFileByRelativePath(MODULE_BASE_PATH);
}
@NotNull
public static List<String> getAllModules(@NotNull Project project) {
List<String> modules = new ArrayList<>();
VirtualFile baseDir = getModuleBaseDir(project);
if (baseDir != null && baseDir.isDirectory()) {
for (VirtualFile child : baseDir.getChildren()) {
if (child.isDirectory()) {
modules.add(child.getName());
}
}
}
return modules;
}
@NotNull
public static List<String> getFrmlFiles(@NotNull Project project, @NotNull String moduleName) {
List<String> files = new ArrayList<>();
VirtualFile baseDir = getModuleBaseDir(project);
if (baseDir != null) {
VirtualFile frmDir = baseDir.findFileByRelativePath(moduleName + "/view/frm");
if (frmDir != null && frmDir.isDirectory()) {
for (VirtualFile child : frmDir.getChildren()) {
if (!child.isDirectory() && child.getName().endsWith(".frml")) {
files.add(child.getNameWithoutExtension());
}
}
}
}
return files;
}
@Nullable
public static PsiDirectory findModuleDirectory(@NotNull Project project, @NotNull String moduleName) {
VirtualFile baseDir = getModuleBaseDir(project);
if (baseDir != null) {
VirtualFile moduleDir = baseDir.findFileByRelativePath(moduleName);
if (moduleDir != null && moduleDir.isDirectory()) {
return PsiManager.getInstance(project).findDirectory(moduleDir);
}
}
return null;
}
@Nullable
public static PsiFile findFrmlFile(@NotNull Project project, @NotNull String moduleName, @NotNull String frmlName) {
VirtualFile baseDir = getModuleBaseDir(project);
if (baseDir != null) {
VirtualFile frmFile = baseDir.findFileByRelativePath(moduleName + "/view/frm/" + frmlName + ".frml");
if (frmFile != null && !frmFile.isDirectory()) {
return PsiManager.getInstance(project).findFile(frmFile);
}
}
return null;
}
}

View File

@@ -0,0 +1,100 @@
package com.sdk.dynform.tools.helper;
import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class DynFormReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(PlatformPatterns.psiElement(PsiLiteralExpression.class),
new PsiReferenceProvider() {
@NotNull
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
PsiLiteralExpression literal = (PsiLiteralExpression) element;
String value = literal.getValue() instanceof String ? (String) literal.getValue() : null;
if (value == null) return PsiReference.EMPTY_ARRAY;
PsiNewExpression newExp = getNewDynFormExpression(literal);
if (newExp != null) {
PsiExpressionList argList = newExp.getArgumentList();
if (argList != null) {
PsiExpression[] args = argList.getExpressions();
// Argument index 3 is moduleName
if (args.length >= 4 && args[3] == literal) {
return new PsiReference[]{new DynFormModuleReference(element, new TextRange(1, value.length() + 1), value)};
}
// Argument index 4 is frmlName
if (args.length >= 5 && args[4] == literal) {
String moduleName = getArgumentValue(args, 3);
if (moduleName != null) {
return new PsiReference[]{new DynFormFrmlReference(element, new TextRange(1, value.length() + 1), moduleName, value)};
}
}
}
}
return PsiReference.EMPTY_ARRAY;
}
});
}
@Nullable
private PsiNewExpression getNewDynFormExpression(PsiLiteralExpression literal) {
PsiElement parent = literal.getParent();
if (parent instanceof PsiExpressionList) {
PsiElement grandParent = parent.getParent();
if (grandParent instanceof PsiNewExpression newExp) {
PsiJavaCodeReferenceElement classRef = newExp.getClassReference();
if (classRef != null && "DynForm".equals(classRef.getReferenceName())) {
return newExp;
}
}
}
return null;
}
@Nullable
private String getArgumentValue(PsiExpression[] args, int index) {
if (args.length > index && args[index] instanceof PsiLiteralExpression literal) {
Object val = literal.getValue();
return val instanceof String ? (String) val : null;
}
return null;
}
private static class DynFormModuleReference extends PsiReferenceBase<PsiElement> {
private final String moduleName;
public DynFormModuleReference(@NotNull PsiElement element, TextRange textRange, String moduleName) {
super(element, textRange);
this.moduleName = moduleName;
}
@Nullable
@Override
public PsiElement resolve() {
return DynFormPathUtils.findModuleDirectory(myElement.getProject(), moduleName);
}
}
private static class DynFormFrmlReference extends PsiReferenceBase<PsiElement> {
private final String moduleName;
private final String frmlName;
public DynFormFrmlReference(@NotNull PsiElement element, TextRange textRange, String moduleName, String frmlName) {
super(element, textRange);
this.moduleName = moduleName;
this.frmlName = frmlName;
}
@Nullable
@Override
public PsiElement resolve() {
return DynFormPathUtils.findFrmlFile(myElement.getProject(), moduleName, frmlName);
}
}
}

View File

@@ -0,0 +1,41 @@
package com.sdk.dynform.tools.helper;
import com.intellij.ide.highlighter.XmlLikeFileType;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.fileTypes.LanguageFileType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
public class FRMLFileType extends LanguageFileType {
public static final FRMLFileType INSTANCE = new FRMLFileType();
private FRMLFileType() {
super(XMLLanguage.INSTANCE);
}
@NotNull
@Override
public String getName() {
return "FRML";
}
@NotNull
@Override
public String getDescription() {
return "DynForm framework blueprint file";
}
@NotNull
@Override
public String getDefaultExtension() {
return "frml";
}
@Nullable
@Override
public Icon getIcon() {
return com.intellij.icons.AllIcons.FileTypes.Xml;
}
}

View File

@@ -0,0 +1,162 @@
package com.sdk.dynform.tools.i18n;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.XmlPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlToken;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
public class I18nCompletionContributor extends CompletionContributor {
private static final Logger LOG = Logger.getInstance(I18nCompletionContributor.class);
public I18nCompletionContributor() {
// Completion for Java/JSP/JS $M.get("...")
extend(CompletionType.BASIC,
PlatformPatterns.psiElement(),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
PsiLiteralExpression literal = PsiTreeUtil.getParentOfType(position, PsiLiteralExpression.class);
// ในบางภาษาอาจจะไม่ใช่ PsiLiteralExpression โดยตรง
if (literal != null && isMGetArgument(literal)) {
String value = literal.getValue() instanceof String ? (String) literal.getValue() : "";
int offsetInLiteral = parameters.getOffset() - literal.getTextRange().getStartOffset() - 1; // -1 for starting quote
if (offsetInLiteral >= 0 && offsetInLiteral <= value.length()) {
handleMultiKeyCompletion(parameters, resultSet, value.substring(0, offsetInLiteral));
}
} else {
// ลองเช็คแบบข้อความดิบ (สำหรับ JS/JSP บางกรณี)
String textBefore = parameters.getEditor().getDocument().getText(
new com.intellij.openapi.util.TextRange(Math.max(0, parameters.getOffset() - 100), parameters.getOffset())
);
java.util.regex.Matcher m = java.util.regex.Pattern.compile("\\$M\\.get\\([\"']([^\"']*)$").matcher(textBefore);
if (m.find()) {
handleMultiKeyCompletion(parameters, resultSet, m.group(1));
}
}
}
});
// Completion for FRML (XML) caption="...", label="..."
extend(CompletionType.BASIC,
XmlPatterns.psiElement().inside(XmlPatterns.xmlAttributeValue()),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
XmlAttribute attribute = PsiTreeUtil.getParentOfType(position, XmlAttribute.class);
if (attribute != null) {
String name = attribute.getName();
if (name.equals("CAPTION") || name.equals("LABEL")) {
XmlAttributeValue valueElement = attribute.getValueElement();
if (valueElement != null) {
int offsetInValue = parameters.getOffset() - valueElement.getTextRange().getStartOffset() - 1;
String value = attribute.getValue();
if (value != null && offsetInValue >= 0 && offsetInValue <= value.length()) {
handleMultiKeyCompletion(parameters, resultSet, value.substring(0, offsetInValue));
} else {
addMessageKeys(parameters, resultSet);
}
}
}
}
}
});
// Completion for @M{...} pattern in XML text or any other context
extend(CompletionType.BASIC,
PlatformPatterns.psiElement(),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
String text = position.getText();
int offsetInElement = parameters.getOffset() - position.getTextRange().getStartOffset();
if (offsetInElement < 0 || offsetInElement > text.length()) return;
String before = text.substring(0, offsetInElement);
// Find the start of the current @M{ block
int openBracket = before.lastIndexOf("@M{");
if (openBracket != -1) {
String contentAfterBracket = before.substring(openBracket + 3);
// If we haven't closed the bracket yet
if (!contentAfterBracket.contains("}")) {
handleMultiKeyCompletion(parameters, resultSet, contentAfterBracket);
}
}
}
});
}
private void handleMultiKeyCompletion(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet resultSet, String content) {
// Find the last separator to determine what the user is currently typing
int lastPlus = content.lastIndexOf('+');
int lastPipe = content.lastIndexOf("||");
int lastSpace = content.lastIndexOf(' ');
int lastHash = content.lastIndexOf('#');
int lastSeparator = Math.max(lastPlus, Math.max(lastPipe != -1 ? lastPipe + 1 : -1, Math.max(lastSpace, lastHash)));
String currentPrefix;
if (lastSeparator != -1) {
// Handle || which is 2 chars
if (lastPipe != -1 && lastSeparator == lastPipe + 1) {
currentPrefix = content.substring(lastPipe + 2);
} else {
currentPrefix = content.substring(lastSeparator + 1);
}
} else {
currentPrefix = content;
}
addMessageKeys(parameters, resultSet.withPrefixMatcher(currentPrefix));
}
private void addMessageKeys(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet resultSet) {
Set<String> keys = I18nUtils.findAllMessageKeys(parameters.getEditor().getProject());
for (String key : keys) {
String translation = I18nUtils.findMessageValue(parameters.getEditor().getProject(), key);
resultSet.addElement(LookupElementBuilder.create(key)
.withTypeText(translation != null ? translation : "", true)
.withIcon(com.intellij.openapi.util.IconLoader.getIcon("/META-INF/pluginIcon.svg", I18nCompletionContributor.class)));
}
}
private boolean isMGetArgument(PsiLiteralExpression literal) {
PsiElement parent = literal.getParent();
if (parent instanceof com.intellij.psi.PsiExpressionList) {
PsiElement grandParent = parent.getParent();
if (grandParent instanceof PsiMethodCallExpression) {
PsiMethodCallExpression methodCall = (PsiMethodCallExpression) grandParent;
String methodName = methodCall.getMethodExpression().getReferenceName();
if ("get".equals(methodName)) {
PsiElement qualifier = methodCall.getMethodExpression().getQualifier();
return qualifier != null && "$M".equals(qualifier.getText());
}
}
}
return false;
}
}

View File

@@ -0,0 +1,86 @@
package com.sdk.dynform.tools.i18n;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.util.NlsContexts;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
public class I18nConfigurable implements Configurable {
private JRadioButton foldingBtn;
private JRadioButton inlayBtn;
private JRadioButton disabledBtn;
private JCheckBox showIconCheck;
@Override
public @NlsContexts.ConfigurableName String getDisplayName() {
return "DynForm I18n Tools";
}
@Override
public @Nullable JComponent createComponent() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.add(new JLabel("Message Display Mode:"));
foldingBtn = new JRadioButton("Folding (Hide key, show translation)");
inlayBtn = new JRadioButton("Inlay Hints (Show translation next to key)");
disabledBtn = new JRadioButton("Disabled");
ButtonGroup group = new ButtonGroup();
group.add(foldingBtn);
group.add(inlayBtn);
group.add(disabledBtn);
panel.add(foldingBtn);
panel.add(inlayBtn);
panel.add(disabledBtn);
panel.add(Box.createVerticalStrut(10));
showIconCheck = new JCheckBox("Show icon in code completion");
panel.add(showIconCheck);
panel.add(Box.createVerticalGlue());
return panel;
}
@Override
public boolean isModified() {
I18nSettings settings = I18nSettings.getInstance();
I18nSettings.DisplayMode currentMode = getCurrentModeFromUI();
return settings.displayMode != currentMode || settings.showIcon != showIconCheck.isSelected();
}
@Override
public void apply() {
I18nSettings settings = I18nSettings.getInstance();
settings.displayMode = getCurrentModeFromUI();
settings.showIcon = showIconCheck.isSelected();
// Refresh all editors to apply changes
com.intellij.codeInsight.daemon.DaemonCodeAnalyzer.getInstance(
com.intellij.openapi.project.ProjectManager.getInstance().getOpenProjects()[0]
).restart();
}
private I18nSettings.DisplayMode getCurrentModeFromUI() {
if (foldingBtn.isSelected()) return I18nSettings.DisplayMode.FOLDING;
if (inlayBtn.isSelected()) return I18nSettings.DisplayMode.INLAY_HINTS;
return I18nSettings.DisplayMode.DISABLED;
}
@Override
public void reset() {
I18nSettings settings = I18nSettings.getInstance();
switch (settings.displayMode) {
case FOLDING: foldingBtn.setSelected(true); break;
case INLAY_HINTS: inlayBtn.setSelected(true); break;
case DISABLED: disabledBtn.setSelected(true); break;
}
showIconCheck.setSelected(settings.showIcon);
}
}

View File

@@ -0,0 +1,109 @@
package com.sdk.dynform.tools.i18n;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingBuilderEx;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class I18nFoldingBuilder extends FoldingBuilderEx {
@NotNull
@Override
public FoldingDescriptor @NotNull [] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
if (I18nSettings.getInstance().displayMode != I18nSettings.DisplayMode.FOLDING) {
return new FoldingDescriptor[0];
}
List<FoldingDescriptor> descriptors = new ArrayList<>();
Project project = root.getProject();
// Check for Java/JSP $M.get("key")
Collection<PsiLiteralExpression> literalExpressions = PsiTreeUtil.findChildrenOfType(root, PsiLiteralExpression.class);
for (PsiLiteralExpression literal : literalExpressions) {
String value = literal.getValue() instanceof String ? (String) literal.getValue() : null;
if (value != null && isMGetArgument(literal)) {
String translation = I18nUtils.findMessageValue(project, value);
if (translation != null) {
descriptors.add(new FoldingDescriptor(literal.getNode(), literal.getTextRange(), null, translation));
}
}
}
// Check for XML/FRML @M{key} in any XML Token
Collection<com.intellij.psi.xml.XmlToken> xmlTokens = PsiTreeUtil.findChildrenOfType(root, com.intellij.psi.xml.XmlToken.class);
for (com.intellij.psi.xml.XmlToken token : xmlTokens) {
String text = token.getText();
if (text != null && text.contains("@M{")) {
java.util.regex.Matcher matcher = I18nUtils.getMPattern().matcher(text);
while (matcher.find()) {
String key = matcher.group(1);
if (key != null) {
String translation = I18nUtils.findMessageValue(project, key);
if (translation != null) {
TextRange range = new TextRange(token.getTextRange().getStartOffset() + matcher.start(),
token.getTextRange().getStartOffset() + matcher.end());
descriptors.add(new FoldingDescriptor(token.getNode(), range, null, translation));
}
}
}
}
}
Collection<XmlAttribute> xmlAttributes = PsiTreeUtil.findChildrenOfType(root, XmlAttribute.class);
for (XmlAttribute attribute : xmlAttributes) {
String name = attribute.getName();
if (name.equals("CAPTION") || name.equals("LABEL")) {
XmlAttributeValue valueElement = attribute.getValueElement();
if (valueElement != null) {
String value = attribute.getValue();
if (value != null && !value.contains("@M{")) {
String translation = I18nUtils.findMessageValue(project, value);
if (translation != null) {
descriptors.add(new FoldingDescriptor(valueElement.getNode(), valueElement.getTextRange(), null, translation));
}
}
}
}
}
return descriptors.toArray(new FoldingDescriptor[0]);
}
private boolean isMGetArgument(PsiLiteralExpression literal) {
PsiElement parent = literal.getParent();
if (parent instanceof PsiExpressionList) {
PsiElement grandParent = parent.getParent();
if (grandParent instanceof PsiMethodCallExpression methodCall) {
String methodName = methodCall.getMethodExpression().getReferenceName();
if ("get".equals(methodName)) {
PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression();
return qualifier != null && "$M".equals(qualifier.getText());
}
}
}
return false;
}
@Nullable
@Override
public String getPlaceholderText(@NotNull ASTNode node) {
return "...";
}
@Override
public boolean isCollapsedByDefault(@NotNull ASTNode node) {
return true;
}
}

View File

@@ -0,0 +1,155 @@
package com.sdk.dynform.tools.i18n;
import com.intellij.codeInsight.hints.*;
import com.intellij.lang.Language;
import com.intellij.openapi.editor.Editor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlToken;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
@SuppressWarnings("UnstableApiUsage")
public class I18nInlayHintsProvider implements InlayHintsProvider<NoSettings> {
@Nullable
@Override
public InlayHintsCollector getCollectorFor(@NotNull PsiFile file, @NotNull Editor editor, @NotNull NoSettings settings, @NotNull InlayHintsSink sink) {
if (I18nSettings.getInstance().displayMode != I18nSettings.DisplayMode.INLAY_HINTS) {
return null;
}
// Prevent duplicate hints: only provide hints if the language being queried
// matches the primary language of the file.
if (!file.getLanguage().equals(file.getViewProvider().getBaseLanguage())) {
return null;
}
return new I18nCollector(editor);
}
private static class I18nCollector extends FactoryInlayHintsCollector {
public I18nCollector(@NotNull Editor editor) {
super(editor);
}
@Override
public boolean collect(@NotNull PsiElement element, @NotNull Editor editor, @NotNull InlayHintsSink sink) {
String lang = element.getLanguage().getDisplayName().toLowerCase();
Language baseLang = element.getLanguage().getBaseLanguage();
String checkLangs = "java#jsp#javascript#ecmascript#js#ecmascript 6";
// 1. Java/JSP/JS $M.get("key")
boolean isJsOrJava = checkLangs.contains(lang) || (baseLang != null && checkLangs.contains(baseLang.getDisplayName().toLowerCase()));
if (isJsOrJava) {
String elementText = element.getText();
if (elementText != null && elementText.startsWith("$M.get")) {
java.util.regex.Matcher matcher = I18nUtils.getMGetPattern().matcher(elementText);
while (matcher.find()) {
String key = matcher.group(1);
if (key != null) {
String translation = I18nUtils.findMessageValue(element.getProject(), key);
if (translation != null) {
sink.addInlineElement(element.getTextRange().getStartOffset() + matcher.end(), true,
getFactory().text(" \u00AB " + translation + " \u00BB"), false);
}
}
}
}
}
// 2. FRML XML @M{}
if ("xml#frml".contains(lang) && element instanceof XmlToken token) {
String text = token.getText();
if (text != null) {
// Handle @M{key}
if (text.contains("@M{")) {
java.util.regex.Matcher matcher = I18nUtils.getMPattern().matcher(text);
while (matcher.find()) {
String key = matcher.group(1);
if (key != null) {
String translation = I18nUtils.findMessageValue(element.getProject(), key);
if (translation != null) {
sink.addInlineElement(token.getTextRange().getStartOffset() + matcher.end(), true,
getFactory().text(" \u00AB " + translation + " \u00BB"), false);
}
}
}
}
// Handle $M.get in XML (e.g. inside script tags or attributes)
if (text.contains("$M.get")) {
java.util.regex.Matcher matcher = I18nUtils.getMGetPattern().matcher(text);
while (matcher.find()) {
String key = matcher.group(1);
if (key != null) {
String translation = I18nUtils.findMessageValue(element.getProject(), key);
if (translation != null) {
sink.addInlineElement(token.getTextRange().getStartOffset() + matcher.end(), true,
getFactory().text(" \u00AB " + translation + " \u00BB"), false);
}
}
}
}
}
}
// 3. FRML XML CAPTION/LABEL
if ("xml#frml".contains(lang) && element instanceof XmlAttribute attribute) {
String name = attribute.getName();
if (name.equals("CAPTION") || name.equals("LABEL")) {
String value = attribute.getValue();
if (value != null && !value.contains("@M{")) {
String translation = I18nUtils.findMessageValue(element.getProject(), value);
if (translation != null) {
sink.addInlineElement(attribute.getTextRange().getEndOffset(), true,
getFactory().text(" \u00AB " + translation + " \u00BB"), false);
}
}
}
}
return true;
}
}
@NotNull
@Override
public NoSettings createSettings() {
return new NoSettings();
}
@NotNull
@Override
public String getName() {
return "DynForm I18n Tools";
}
@NotNull
@Override
public SettingsKey<NoSettings> getKey() {
return new SettingsKey<>("dynform.i18n.tools");
}
@NotNull
@Override
public ImmediateConfigurable createConfigurable(@NotNull NoSettings settings) {
return listener -> new JPanel();
}
@Override
public @Nullable String getPreviewText() {
return null;
}
@Override
public boolean isLanguageSupported(@NotNull com.intellij.lang.Language language) {
String id = language.getID();
return id.equals("JAVA") || id.equals("XML") || id.equals("JSP") ||
id.contains("JavaScript") || id.contains("JS") ||
id.contains("ECMAScript") || id.contains("TypeScript");
}
}

View File

@@ -0,0 +1,200 @@
package com.sdk.dynform.tools.i18n;
import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.XmlPatterns;
import com.intellij.psi.*;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class I18nReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
// 1. Java Reference Provider (Specific for Java PSI)
registrar.registerReferenceProvider(PlatformPatterns.psiElement(PsiLiteralExpression.class),
new PsiReferenceProvider() {
@NotNull
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
PsiLiteralExpression literalExpression = (PsiLiteralExpression) element;
String value = literalExpression.getValue() instanceof String ? (String) literalExpression.getValue() : null;
if (value != null && isMGetArgument(literalExpression)) {
return new PsiReference[]{new I18nReference(element, new TextRange(1, value.length() + 1), value)};
}
return PsiReference.EMPTY_ARRAY;
}
});
// 2. XML Attribute Reference Provider (for .frml CAPTION/LABEL)
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("CAPTION", "LABEL")),
new PsiReferenceProvider() {
@NotNull
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
XmlAttributeValue attributeValue = (XmlAttributeValue) element;
String value = attributeValue.getValue();
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
List<PsiReference> refs = new ArrayList<>();
Pattern sepPattern = Pattern.compile("(\\|\\||[\\s+#])");
Matcher matcher = sepPattern.matcher(value);
int lastEnd = 0;
while (matcher.find()) {
addRef(refs, element, value, lastEnd, matcher.start(), 1);
lastEnd = matcher.end();
}
addRef(refs, element, value, lastEnd, value.length(), 1);
return refs.toArray(new PsiReference[0]);
}
});
// 3. Generic Reference Provider for @M{} and $M.get()
// Works for JavaScript (including ES6), JSP, and XML/FRML text/tokens
registrar.registerReferenceProvider(PlatformPatterns.psiElement(),
new PsiReferenceProvider() {
@NotNull
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
// Avoid duplicates if already handled
if (element instanceof PsiLiteralExpression || element instanceof XmlAttributeValue) {
return PsiReference.EMPTY_ARRAY;
}
String text = element.getText();
if (text == null || text.isEmpty()) return PsiReference.EMPTY_ARRAY;
List<PsiReference> refs = new ArrayList<>();
// CASE A: Element is a string literal (quoted)
if ((text.startsWith("\"") || text.startsWith("'")) && text.length() > 2) {
if (isInsideMGet(element)) {
String key = text.substring(1, text.length() - 1);
if (!key.isEmpty()) {
refs.add(new I18nReference(element, new TextRange(1, text.length() - 1), key));
}
}
}
// CASE B: Element contains @M{key} or $M.get("key")
// This handles cases where the element is a larger token containing the pattern
if (text.contains("@M{")) {
addMultiKeyReferences(refs, element, text, I18nUtils.getMPattern());
}
if (text.contains("$M.get")) {
addMultiKeyReferences(refs, element, text, I18nUtils.getMGetPattern());
}
return refs.isEmpty() ? PsiReference.EMPTY_ARRAY : refs.toArray(new PsiReference[0]);
}
});
}
/**
* Robust check if the element is part of a $M.get(...) call.
* Searches up the tree to handle different PSI structures in JS/JSP/XML.
*/
private boolean isInsideMGet(PsiElement element) {
PsiElement current = element.getParent();
int depth = 0;
// Search up to 3 levels: Argument -> ArgumentList -> CallExpression
while (current != null && depth < 3) {
String txt = current.getText();
if (txt != null && txt.contains("$M.get")) return true;
current = current.getParent();
depth++;
}
return false;
}
private void addMultiKeyReferences(List<PsiReference> refs, PsiElement element, String text, Pattern pattern) {
Matcher mMatcher = pattern.matcher(text);
int elementStart = element.getTextRange().getStartOffset();
while (mMatcher.find()) {
if (mMatcher.groupCount() >= 1) {
String content = mMatcher.group(1);
int contentStartInElement = mMatcher.start(1);
Pattern sepPattern = Pattern.compile("(\\|\\||[\\s+#])");
Matcher sMatcher = sepPattern.matcher(content);
int lastEnd = 0;
while (sMatcher.find()) {
addRef(refs, element, content, lastEnd, sMatcher.start(), contentStartInElement);
lastEnd = sMatcher.end();
}
addRef(refs, element, content, lastEnd, content.length(), contentStartInElement);
}
}
}
private void addRef(List<PsiReference> refs, PsiElement element, String content, int start, int end, int baseOffset) {
if (start < end) {
String key = content.substring(start, end);
if (!key.isEmpty() && !key.equals("-")) {
refs.add(new I18nReference(element, new TextRange(baseOffset + start, baseOffset + end), key));
}
}
}
private boolean isMGetArgument(PsiLiteralExpression literal) {
PsiElement parent = literal.getParent();
if (parent instanceof PsiExpressionList) {
PsiElement grandParent = parent.getParent();
if (grandParent instanceof PsiMethodCallExpression methodCall) {
String methodName = methodCall.getMethodExpression().getReferenceName();
if ("get".equals(methodName)) {
PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression();
return qualifier != null && "$M".equals(qualifier.getText());
}
}
}
return false;
}
public static class I18nReference extends PsiReferenceBase<PsiElement> implements PsiPolyVariantReference {
private final String key;
public I18nReference(@NotNull PsiElement element, TextRange textRange, String key) {
super(element, textRange);
this.key = key;
}
@Override
public boolean isSoft() {
return true;
}
@NotNull
@Override
public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
PsiElement entry = I18nUtils.findMessageEntry(myElement.getProject(), key);
if (entry != null) {
return new ResolveResult[]{new PsiElementResolveResult(entry)};
}
return new ResolveResult[0];
}
@Nullable
@Override
public PsiElement resolve() {
ResolveResult[] resolveResults = multiResolve(false);
return resolveResults.length == 1 ? resolveResults[0].getElement() : null;
}
@NotNull
@Override
public Object @NotNull [] getVariants() {
return new Object[0];
}
}
}

View File

@@ -0,0 +1,40 @@
package com.sdk.dynform.tools.i18n;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@State(
name = "com.sdk.dynform.tools.i18n.I18nSettings",
storages = @Storage("DynamicFormTools.xml")
)
public class I18nSettings implements PersistentStateComponent<I18nSettings> {
public enum DisplayMode {
FOLDING,
INLAY_HINTS,
DISABLED
}
public DisplayMode displayMode = DisplayMode.FOLDING;
public boolean showIcon = true;
public static I18nSettings getInstance() {
return ApplicationManager.getApplication().getService(I18nSettings.class);
}
@Nullable
@Override
public I18nSettings getState() {
return this;
}
@Override
public void loadState(@NotNull I18nSettings state) {
XmlSerializerUtil.copyBean(state, this);
}
}

View File

@@ -0,0 +1,214 @@
package com.sdk.dynform.tools.i18n;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class I18nUtils {
private static final List<String> MESSAGE_FILENAMES = Arrays.asList("message.xml", "message_th.xml", "message_en.xml");
@NotNull
public static List<XmlFile> findMessageFiles(@NotNull Project project) {
List<XmlFile> result = new ArrayList<>();
for (String filename : MESSAGE_FILENAMES) {
Collection<VirtualFile> files = FilenameIndex.getVirtualFilesByName(filename, GlobalSearchScope.everythingScope(project));
for (VirtualFile file : files) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if (psiFile instanceof XmlFile) {
result.add((XmlFile) psiFile);
}
}
}
return result;
}
private static final Map<String, Map<String, String>> cache = new LinkedHashMap<>();
private static long lastCacheUpdate = 0;
private static final long CACHE_TIMEOUT = 5000; // 5 seconds
@Nullable
public static String findMessageValue(@NotNull Project project, @NotNull String key) {
if (key.startsWith("#") || key.startsWith("=")) {
return key.substring(1);
}
updateCache(project);
// Try to find in any of the cached message files
for (Map<String, String> messageMap : cache.values()) {
String value = resolveKey(messageMap, key);
if (value != null) {
return value;
}
}
return null;
}
@Nullable
private static String resolveKey(Map<String, String> messageMap, String key) {
if (messageMap.isEmpty()) return null;
String msgKey = key.toLowerCase();
String[] keys = key.split(" ");
boolean usePlus = false;
if (keys.length == 1) {
keys = msgKey.split("\\+");
usePlus = keys.length > 1;
}
if (keys.length == 1) {
keys = msgKey.split("\\|\\|");
}
if (keys.length > 1) {
StringBuilder result = new StringBuilder(msgKey);
int seq = 0;
for (String id : keys) {
if (id.isBlank()) continue;
String cleanId = id.replace("-", "_").toLowerCase();
String value;
if (id.startsWith("#")) {
value = id.substring(1);
} else if (id.equals("-")) {
value = "-";
} else {
value = messageMap.get(cleanId);
if (value == null) {
// Recursive-like resolve for sub-keys if not found directly
value = resolveKey(messageMap, cleanId);
}
}
if (value != null) {
String target = (seq > 0 && usePlus) ? "+" + id : id;
int start = result.indexOf(target);
if (start != -1) {
result.replace(start, start + target.length(), value);
}
}
seq++;
}
return result.toString().replaceAll("\\|\\|", "");
} else {
msgKey = key.replaceAll("-", "_").toLowerCase();
String result = messageMap.get(msgKey);
if (result == null) {
if (!msgKey.contains(".")) {
result = messageMap.get("sys." + msgKey);
} else {
result = msgKey.substring(msgKey.indexOf(".") + 1);
if (result.isBlank()) {
result = msgKey;
}
result = beautify(result.replace("_", " "));
}
if (result == null) {
result = beautify(msgKey.replace("_", " "));
}
}
return result != null ? result.replaceAll("\\|\\|", "") : null;
}
}
private static String beautify(String str) {
if (str == null || str.isEmpty()) return str;
StringBuilder result = new StringBuilder(str.length());
boolean capitalizeNext = true;
for (char c : str.toCharArray()) {
if (Character.isWhitespace(c) || c == '_') {
capitalizeNext = true;
result.append(' ');
} else if (capitalizeNext) {
result.append(Character.toUpperCase(c));
capitalizeNext = false;
} else {
result.append(Character.toLowerCase(c));
}
}
return result.toString().trim();
}
private static void updateCache(@NotNull Project project) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastCacheUpdate < CACHE_TIMEOUT && !cache.isEmpty()) {
return;
}
cache.clear();
List<XmlFile> messageFiles = findMessageFiles(project);
for (XmlFile messageFile : messageFiles) {
String filePath = messageFile.getVirtualFile().getPath();
Map<String, String> messageMap = new HashMap<>();
XmlTag rootTag = messageFile.getRootTag();
if (rootTag != null) {
for (XmlTag entryTag : rootTag.findSubTags("entry")) {
String entryKey = entryTag.getAttributeValue("key");
if (entryKey != null) {
messageMap.put(entryKey, entryTag.getValue().getText());
}
}
}
cache.put(filePath, messageMap);
}
lastCacheUpdate = currentTime;
}
@Nullable
public static PsiElement findMessageEntry(@NotNull Project project, @NotNull String key) {
List<XmlFile> messageFiles = findMessageFiles(project);
for (XmlFile messageFile : messageFiles) {
XmlTag rootTag = messageFile.getRootTag();
if (rootTag != null) {
for (XmlTag entryTag : rootTag.findSubTags("entry")) {
if (key.equals(entryTag.getAttributeValue("key"))) {
return entryTag.getAttribute("key");
}
}
}
}
return null;
}
@NotNull
public static Set<String> findAllMessageKeys(@NotNull Project project) {
updateCache(project);
Set<String> allKeys = new TreeSet<>();
for (Map<String, String> messageMap : cache.values()) {
allKeys.addAll(messageMap.keySet());
}
return allKeys;
}
private static final java.util.regex.Pattern M_PATTERN = java.util.regex.Pattern.compile("@M\\{(.*?)}");
private static final java.util.regex.Pattern MGET_PATTERN = java.util.regex.Pattern.compile("\\$M\\.get\\(\\s*[\"'](.*?)[\"']\\s*\\)");
@NotNull
public static List<String> extractMessageKeys(@NotNull String text) {
List<String> keys = new ArrayList<>();
java.util.regex.Matcher matcher = M_PATTERN.matcher(text);
while (matcher.find()) {
keys.add(matcher.group(1));
}
return keys;
}
@NotNull
public static java.util.regex.Pattern getMPattern() {
return M_PATTERN;
}
@NotNull
public static java.util.regex.Pattern getMGetPattern() {
return MGET_PATTERN;
}
}