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:
154
src/main/java/com/sdk/dynform/helper/GUtils.java
Normal file
154
src/main/java/com/sdk/dynform/helper/GUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/main/java/com/sdk/dynform/tools/helper/FRMLFileType.java
Normal file
41
src/main/java/com/sdk/dynform/tools/helper/FRMLFileType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
109
src/main/java/com/sdk/dynform/tools/i18n/I18nFoldingBuilder.java
Normal file
109
src/main/java/com/sdk/dynform/tools/i18n/I18nFoldingBuilder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/sdk/dynform/tools/i18n/I18nSettings.java
Normal file
40
src/main/java/com/sdk/dynform/tools/i18n/I18nSettings.java
Normal 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);
|
||||
}
|
||||
}
|
||||
214
src/main/java/com/sdk/dynform/tools/i18n/I18nUtils.java
Normal file
214
src/main/java/com/sdk/dynform/tools/i18n/I18nUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user