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

2
.idea/.name generated
View File

@@ -1 +1 @@
ActionModelsGenerator
DynamicFormTools

View File

@@ -0,0 +1,10 @@
<ivy-module version="2.0">
<info organisation="bundledModule" module="intellij.json.split" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="intellij.json.split" ext="jar" conf="default" url="plugins/json/lib/modules"/>
</publications>
<dependencies/>
</ivy-module>

View File

@@ -0,0 +1,15 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="JavaScript" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="javascript-plugin" ext="jar" conf="default" url="plugins/javascript-plugin/lib"/>
<artifact name="javascript-frontback" ext="jar" conf="default" url="plugins/javascript-plugin/lib"/>
</publications>
<dependencies>
<dependency org="bundledPlugin" name="com.intellij.modules.json" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.css" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.css" rev="IU-251.23774.435"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,12 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.css" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="css-impl" ext="jar" conf="default" url="plugins/css-impl/lib"/>
</publications>
<dependencies>
<dependency org="bundledPlugin" name="com.intellij.platform.images" rev="IU-251.23774.435"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,12 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.java-i18n" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="java-i18n" ext="jar" conf="default" url="plugins/java-i18n/lib"/>
</publications>
<dependencies>
<dependency org="bundledPlugin" name="com.intellij.properties" rev="IU-251.23774.435"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,17 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.javaee" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="jasper-v2-rt" ext="jar" conf="default" url="plugins/JavaEE/lib"/>
<artifact name="javaee-rt" ext="jar" conf="default" url="plugins/JavaEE/lib"/>
<artifact name="javaee-openapi" ext="jar" conf="default" url="plugins/JavaEE/lib"/>
<artifact name="javaee-platform" ext="jar" conf="default" url="plugins/JavaEE/lib"/>
</publications>
<dependencies>
<dependency org="bundledPlugin" name="com.intellij.java" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.java-i18n" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.properties" rev="IU-251.23774.435"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,12 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.javaee.el" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="javaee-el-core" ext="jar" conf="default" url="plugins/javaee-el-core/lib"/>
</publications>
<dependencies>
<dependency org="bundledPlugin" name="com.intellij.javaee" rev="IU-251.23774.435"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,16 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.javaee.web" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="javaee-web-impl" ext="jar" conf="default" url="plugins/javaee-web-impl/lib"/>
</publications>
<dependencies>
<dependency org="bundledPlugin" name="com.intellij.java" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.java-i18n" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.properties" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.css" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.javaee" rev="IU-251.23774.435"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,13 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.jsp" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="javaee-jsp-base-impl" ext="jar" conf="default" url="plugins/javaee-jsp-base-impl/lib"/>
</publications>
<dependencies>
<dependency org="bundledPlugin" name="com.intellij.javaee.web" rev="IU-251.23774.435"/>
<dependency org="bundledPlugin" name="com.intellij.javaee.el" rev="IU-251.23774.435"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,14 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.modules.json" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="json" ext="jar" conf="default" url="plugins/json/lib"/>
<artifact name="intellij.json.split" ext="jar" conf="default" url="plugins/json/lib/modules"/>
</publications>
<dependencies>
<dependency org="bundledModule" name="intellij.json.split" rev="IU-251.23774.435"/>
<dependency org="bundledModule" name="intellij.json.split" rev="IU-251.23774.435"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,10 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.platform.images" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="platform-images" ext="jar" conf="default" url="plugins/platform-images/lib"/>
</publications>
<dependencies/>
</ivy-module>

View File

@@ -0,0 +1,11 @@
<ivy-module version="2.0">
<info organisation="bundledPlugin" module="com.intellij.properties" revision="IU-251.23774.435"/>
<configurations>
<conf name="default" visibility="public"/>
</configurations>
<publications>
<artifact name="properties" ext="jar" conf="default" url="plugins/properties/lib"/>
<artifact name="properties-frontend" ext="jar" conf="default" url="plugins/properties/lib"/>
</publications>
<dependencies/>
</ivy-module>

View File

@@ -1 +1 @@
2026-04-03
2026-04-07

274
DevResources/Message.java Executable file
View File

@@ -0,0 +1,274 @@
package sdk.i18n;
import com.apps.Constants;
import com.apps.SystemFactory;
import jakarta.servlet.ServletContext;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
import sdk.config.ApiConfig;
import sdk.json.JSONObject;
import sdk.context.AppContext;
import sdk.utils.JUtils;
import sdk.utils.SDKLogger;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
public class Message {
private LinkedHashMap<String, LinkedHashMap<String, String>> msgMaps;
private SystemFactory system;
private LinkedHashMap<String, String> rescMessage;
private final SDKLogger logger;
public Message() {
rescMessage = new LinkedHashMap<>();
logger = new SDKLogger("i18N-Message");
}
public Message(SystemFactory factory) {
this.system = factory;
AppContext app = factory.getAppInstance();
logger = new SDKLogger("i18N-Message");
msgMaps = (LinkedHashMap<String, LinkedHashMap<String, String>>) app.getAttribute(Constants._APP_MESSAGE);
if (msgMaps != null) {
for (String key : msgMaps.keySet()) {
if (msgMaps.get(key) == null) {
msgMaps = null;
break;
}
}
}
if (msgMaps == null || msgMaps.isEmpty() || msgMaps.containsValue(null)) {
msgMaps = new LinkedHashMap<>();
ApiConfig msgAPI = factory.getApi("i18n");
for (ApiConfig.ApiParameter param : msgAPI.getParameters().values()) {
String rescFile = param.getValue();
if (!"".equals(rescFile)) {
LinkedHashMap<String, String> rescMsg = readResource(rescFile);
String lang = param.getName().toUpperCase();
msgMaps.put(lang, rescMsg);
}
}
app.setAttribute(Constants._APP_MESSAGE, msgMaps);
}
String lang = factory.getLanguage("TH");
rescMessage = msgMaps.get(lang.toUpperCase());
if (rescMessage == null) {
rescMessage = msgMaps.get("TH");
}
}
public LinkedHashMap<String,String> getLang(String lang) {
return msgMaps.get(lang.toUpperCase());
}
public String parseExp(String inStr) {
String result = inStr;
// String[] ssnToken = JUtils.getRegxToken(inStr, "@\\{[\\s\\S]+?\\}");
// int idxFactor = 2;
// if (ssnToken.length > 0) {
// DTO_SYSTEM_CONFIG sysData= system.getSysData();
// for (String token : ssnToken) {
// String ssnKey = token.substring(0, token.length() - 1).substring(idxFactor).trim();
// String ssnValue = sysData.getValue(ssnKey, "");
// if (!ssnValue.isBlank()) {
// result = result.replace(token, ssnValue);
// }
// }
// }
String[] ssnToken = JUtils.getRegxToken(result, "@[M]\\{[\\s\\S]+?\\}");
int idxFactor = 3;
for (String token : ssnToken) {
String msgKey = token.substring(0, token.length() - 1).substring(idxFactor).trim();
result = result.replace(token, this.get(msgKey));
}
return result.replaceAll("\\|\\|","");
}
private LinkedHashMap<String, String> readResource(String fileName) {
LinkedHashMap<String, String> result = new LinkedHashMap<>();
SAXBuilder formBuilder = new SAXBuilder();
formBuilder.setFeature("http://xml.org/sax/features/validation", false);
formBuilder.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
formBuilder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
ServletContext context = system.getAppContext();
InputStream rescFile = context.getResourceAsStream(fileName);
try {
Document rescDoc = formBuilder.build(rescFile);
Element rescRoot = rescDoc.getRootElement();
List<Element> msgList = rescRoot.getChildren("entry");
for (Element elMsg : msgList) {
String key = elMsg.getAttributeValue("key", "");
String value = elMsg.getTextTrim();
if (!"".equals(key)) {
value = parseExp(value);
result.put(key.toLowerCase(), value);
}
}
} catch (Exception ex) {
result = null;
logger.error(ex);
}
return result;
}
public String get(String key, String locale) {
if (key.startsWith("#")) {
return key.substring(1);
}
LinkedHashMap<String,String> message = null;
if (locale != null) {
message = getLang(locale.toUpperCase());
}
if (message == null) {
message = rescMessage;
}
if (!message.isEmpty()) {
boolean useDot = false;
String msgKey = key.replaceAll("-", "_").toLowerCase();
String[] keys = msgKey.split(" ");
if (keys.length == 1) {
keys = msgKey.split("\\+");
useDot = keys.length > 1;
}
String result;
if (keys.length > 1) {
result = msgKey;
int seq = 0;
for (String id : keys) {
if (id.isBlank()) continue;
String value = message.get(id);
if (value != null) {
result = result.replace((seq > 0 && useDot) ? "+" + id : id, value);
} else {
value = this.get(id,locale);
result = result.replace(id, value);
}
seq++;
}
} else {
result = message.get(msgKey.toLowerCase());
if (result == null) {
result = JUtils.beautify(msgKey.replace("_", " "));
}
}
return result;
} else {
return key;
}
}
public String get(String key) {
if (key.startsWith("#")||key.startsWith("=")) {
return key.substring(1);
}
if (!rescMessage.isEmpty()) {
boolean useDot = false;
String msgKey = key.toLowerCase();
String[] keys = key.split(" ");
if (keys.length == 1) {
keys = msgKey.split("\\+");
useDot = keys.length > 1;
}
if (keys.length == 1) {
keys = msgKey.split("\\|\\|");
}
String result;
if (keys.length > 1) {
result = msgKey;
int seq = 0;
for (String id : keys) {
String value;
String idKey = id;
if (id.isBlank()) continue;
id = id.replaceAll("-", "_").toLowerCase();
if (idKey.startsWith("#")) {
result = result.replace(id, idKey.substring(1));
} else {
if (!id.equals("-")) {
value = rescMessage.get(id);
} else {
value = "-";
}
if (value != null) {
result = result.replace((seq > 0 && useDot) ? "+" + id : id, value);
} else {
value = this.get(id);
result = result.replace((seq > 0 && useDot) ? "+" + id : id, value);
result = result.replace(id, value);
}
}
seq ++;
}
} else {
msgKey = key.replaceAll("-","_").toLowerCase();
result = rescMessage.get(msgKey.toLowerCase());
if (result == null) {
if (!msgKey.contains(".")) {
result = rescMessage.get("sys."+msgKey.toLowerCase());
} else {
result = msgKey.substring(msgKey.indexOf(".")+1);
if (result.isBlank()) {
result = msgKey;
}
result = JUtils.beautify(result.replace("_", " "));
}
result = (result == null) ? JUtils.beautify(msgKey.replace("_", " ")) : result;
}
}
return result.replaceAll("\\|\\|","");
} else {
return key;
}
}
public String buildJson(String lang) {
LinkedHashMap<String, String> langMsg = msgMaps.get(lang.toUpperCase());
if (langMsg == null) {
msgMaps.get("TH");
}
JSONObject jsResult = new JSONObject();
SortedSet<String> keys = new TreeSet<>(langMsg.keySet());
for (String key : keys) {
String value = langMsg.get(key);
jsResult.put(key,value);
}
return jsResult.toString();
}
public String buildObject(String lang) {
LinkedHashMap<String, String> langMsg = msgMaps.get(lang.toUpperCase());
if (langMsg == null) {
langMsg = msgMaps.get("TH");
}
if (langMsg == null) {
langMsg = msgMaps.firstEntry().getValue();
}
JSONObject jsResult = new JSONObject();
langMsg.forEach(jsResult::put);
return jsResult.toString(4);
}
}

View File

@@ -1,8 +1,8 @@
# ActionModelsGenerator
# DynamicFormTools
## Project Overview
The `ActionModelsGenerator` is an IntelliJ Platform Plugin designed to automate the generation of Java `ActionBean` and Data Transfer Object (DTO) classes from a database schema. This plugin integrates directly into the IntelliJ IDE, allowing developers to select a database table and generate corresponding Java classes based on predefined FreeMarker templates.
The `DynamicFormTools` is an IntelliJ Platform Plugin designed to automate the generation of Java `ActionBean` and Data Transfer Object (DTO) classes from a database schema. This plugin integrates directly into the IntelliJ IDE, allowing developers to select a database table and generate corresponding Java classes based on predefined FreeMarker templates.
**Key Features:**
* **Database Schema Introspection:** Utilizes IntelliJ's database tools to read table and column metadata.

View File

@@ -1,8 +1,8 @@
# ActionModelsGenerator
# DynamicFormTools
## Project Overview
The `ActionModelsGenerator` is an IntelliJ Platform Plugin designed to automate the generation of Java `ActionBean` and Data Transfer Object (DTO) classes from a database schema. This plugin integrates directly into the IntelliJ IDE, allowing developers to select a database table and generate corresponding Java classes based on predefined FreeMarker templates.
The `DynamicFormTools` is an IntelliJ Platform Plugin designed to automate the generation of Java `ActionBean` and Data Transfer Object (DTO) classes from a database schema. This plugin integrates directly into the IntelliJ IDE, allowing developers to select a database table and generate corresponding Java classes based on predefined FreeMarker templates.
**Key Features:**
* **Database Schema Introspection:** Utilizes IntelliJ's database tools to read table and column metadata.

View File

@@ -4,7 +4,7 @@ plugins {
id("org.jetbrains.intellij.platform") version "2.7.0"
}
group = "com.sdk.generators.actionmodels.v3"
group = "com.sdk.dynform.tools"
version = "2.1.2"
repositories {
@@ -24,6 +24,8 @@ dependencies {
// Add necessary plugin dependencies for compilation here, example:
bundledPlugin("com.intellij.java")
bundledPlugin("com.intellij.database")
bundledPlugin("com.intellij.jsp")
bundledPlugin("JavaScript")
}
implementation("org.freemarker:freemarker:2.3.32")

2
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{
"name": "ActionModelsGenerator",
"name": "DynamicFormTools",
"lockfileVersion": 3,
"requires": true,
"packages": {}

View File

@@ -1 +1 @@
rootProject.name = "ActionModelsGenerator"
rootProject.name = "DynamicFormTools"

View File

@@ -1,4 +1,4 @@
package com.sdk.generators;
package com.sdk.dynform.helper;
import com.intellij.database.model.DasColumn;
import com.intellij.database.model.DasObject;
@@ -139,15 +139,15 @@ public class GUtils {
ApplicationManager.getApplication().invokeLater(() -> Messages.showErrorDialog(project, message, "Generation Failed"));
NotificationGroupManager.getInstance()
// This ID should be registered in your plugin.xml
.getNotificationGroup("Action-Models-Generator-Notification")
.createNotification("ActionModels Generator", message, NotificationType.ERROR)
.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("Action-Models-Generator-Notification")
.getNotificationGroup("Dynamic-Form-Tools-Notification")
.createNotification(title, content, NotificationType.INFORMATION)
.notify(project);
}

View File

@@ -1,4 +1,4 @@
package com.sdk.generators.actionmodels;
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.model.DasColumn;
import com.intellij.database.model.ObjectKind;
@@ -18,7 +18,7 @@ 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.generators.GUtils;
import com.sdk.dynform.helper.GUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;

View File

@@ -1,4 +1,4 @@
package com.sdk.generators.actionmodels;
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.psi.DbTable;
import com.intellij.openapi.actionSystem.AnAction;
@@ -15,7 +15,7 @@ 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.generators.GUtils;
import com.sdk.dynform.helper.GUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;

View File

@@ -1,4 +1,4 @@
package com.sdk.generators.actionmodels;
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.psi.DbTable;
import com.intellij.openapi.actionSystem.AnAction;
@@ -15,7 +15,7 @@ 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.generators.GUtils;
import com.sdk.dynform.helper.GUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;

View File

@@ -1,4 +1,4 @@
package com.sdk.generators.actionmodels;
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.psi.DbTable;
import com.intellij.openapi.actionSystem.AnAction;
@@ -15,7 +15,7 @@ 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.generators.GUtils;
import com.sdk.dynform.helper.GUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;

View File

@@ -1,4 +1,4 @@
package com.sdk.generators.actionmodels;
package com.sdk.dynform.tools.generators.actionmodels;
import com.intellij.database.model.DasColumn;
import com.intellij.database.model.DasTableKey;
@@ -9,7 +9,7 @@ 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.generators.GUtils;
import com.sdk.dynform.helper.GUtils;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;

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;
}
}

View File

@@ -1,12 +1,12 @@
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin>
<!-- Unique id for this plugin. Must stay constant for the life of the plugin. -->
<id>com.sdk.generators.actionmodels</id>
<name>Database Action Models Generator</name>
<id>com.sdk.dynform.tools</id>
<name>Dynamic Form Helper</name>
<vendor>Sakda Sakprapakorn</vendor>
<description><![CDATA[
<h3>Automate Backend Boilerplate with Database Action Models Generator</h3>
<h3>Automate Backend Boilerplate with Dynamic form tools</h3>
<p>This plugin streamlines development in Java-based web applications (such as <code>vrms-system</code> and <code>teddy-taxi-web</code>) by automating the generation of <b>ActionBean</b>, <b>DTO</b>, and <b>Dataset XML</b> files directly from your database schema.</p>
<h4>Key Features:</h4>
@@ -65,7 +65,7 @@
</ul>
<h3>Refactoring and Improvements</h3>
<ul>
<li><strong>Project Structure Refactoring:</strong> The project structure has been reorganized. Generator-related classes were moved to a new package (<code>com.sdk.generators.actionmodels</code>), and template directory names were standardized to <code>src/main/resources/templates</code>.</li>
<li><strong>Project Structure Refactoring:</strong> The project structure has been reorganized. Generator-related classes were moved to a new package (<code>com.sdk.dynform.tools.generators.actionmodels</code>), and template directory names were standardized to <code>src/main/resources/templates</code>.</li>
<li><strong>Build System Updates:</strong> Updated <code>build.gradle.kts</code>, <code>plugin.xml</code>, and Gradle wrapper files to reflect the structural and functional enhancements.</li>
</ul>
]]></change-notes>
@@ -73,30 +73,68 @@
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.java</depends>
<depends>com.intellij.database</depends>
<depends>com.intellij.jsp</depends>
<depends>com.intellij.modules.xml</depends>
<depends>com.intellij.modules.javascript</depends>
<depends>JavaScript</depends>
<actions>
<group id="com.sdk.dblogergen.actions.AuditGroup" popup="true" text="Generate Action Models">
<group id="com.sdk.dynform.tools.generators.actionmodels.GeneratorGroup" popup="true" text="Generate Action Models">
<add-to-group group-id="DatabaseViewPopupMenu" anchor="first"/>
<action id="com.sdk.generators.actionmodels.GenerateBeanAction"
class="com.sdk.generators.actionmodels.GenerateBeanAction"
<action id="com.sdk.dynform.tools.generators.actionmodels.GenerateBeanAction"
class="com.sdk.dynform.tools.generators.actionmodels.GenerateBeanAction"
text="Generate Action Models V2"
description="Generates ActionBean classes from a database schema.">
</action>
<action id="com.sdk.generators.actionmodels.GenerateBeanAction.v3"
class="com.sdk.generators.actionmodels.GenerateBeanActionV3"
<action id="com.sdk.dynform.tools.generators.actionmodels.GenerateBeanAction.v3"
class="com.sdk.dynform.tools.generators.actionmodels.GenerateBeanActionV3"
text="Generate Action Models V3"
description="Generates ActionBean classes from a database schema V3 (sdk.db.xxx).">
</action>
<action id="com.sdk.generators.actionmodels.GenerateDatasetAction"
class="com.sdk.generators.actionmodels.GenerateDatasetAction"
<action id="com.sdk.dynform.tools.generators.actionmodels.GenerateDatasetAction"
class="com.sdk.dynform.tools.generators.actionmodels.GenerateDatasetAction"
text="Generate Dataset XML"
description="Generates Dataset XML definition from a database table.">
</action>
</group>
</actions>
<!-- in plugin.xml, inside the <extensions defaultExtensionNs="com.intellij"> tag -->
<extensions defaultExtensionNs="com.intellij">
<notificationGroup id="Action-Models-Generator-Notification" displayType="BALLOON" isLogByDefault="true"/>
<applicationService serviceImplementation="com.sdk.dynform.tools.i18n.I18nSettings"/>
<applicationConfigurable instance="com.sdk.dynform.tools.i18n.I18nConfigurable"
id="com.sdk.dynform.tools.i18n.I18nConfigurable"
displayName="DynForm I18n Tools"/>
<fileType name="FRML" implementationClass="com.sdk.dynform.tools.helper.FRMLFileType" extensions="frml" language="XML"/>
<notificationGroup id="Dynamic-Form-Tools-Notification" displayType="BALLOON" isLogByDefault="true"/>
<!-- Folding Builders -->
<lang.foldingBuilder language="JAVA" implementationClass="com.sdk.dynform.tools.i18n.I18nFoldingBuilder"/>
<lang.foldingBuilder language="XML" implementationClass="com.sdk.dynform.tools.i18n.I18nFoldingBuilder"/>
<lang.foldingBuilder language="JavaScript" implementationClass="com.sdk.dynform.tools.i18n.I18nFoldingBuilder"/>
<!-- Inlay Hints Providers -->
<codeInsight.inlayProvider language="JAVA" implementationClass="com.sdk.dynform.tools.i18n.I18nInlayHintsProvider"/>
<codeInsight.inlayProvider language="XML" implementationClass="com.sdk.dynform.tools.i18n.I18nInlayHintsProvider"/>
<codeInsight.inlayProvider language="JavaScript" implementationClass="com.sdk.dynform.tools.i18n.I18nInlayHintsProvider"/>
<!-- Reference Contributors -->
<psi.referenceContributor language="JAVA" implementation="com.sdk.dynform.tools.i18n.I18nReferenceContributor"/>
<psi.referenceContributor language="XML" implementation="com.sdk.dynform.tools.i18n.I18nReferenceContributor"/>
<psi.referenceContributor language="JavaScript" implementation="com.sdk.dynform.tools.i18n.I18nReferenceContributor"/>
<psi.referenceContributor language="JAVA" implementation="com.sdk.dynform.tools.helper.DynFormReferenceContributor"/>
<psi.referenceContributor language="XML" implementation="com.sdk.dynform.tools.helper.DynFormReferenceContributor"/>
<psi.referenceContributor language="JavaScript" implementation="com.sdk.dynform.tools.helper.DynFormReferenceContributor"/>
<!-- Completion Contributors -->
<completion.contributor language="JAVA" implementationClass="com.sdk.dynform.tools.i18n.I18nCompletionContributor"/>
<completion.contributor language="XML" implementationClass="com.sdk.dynform.tools.i18n.I18nCompletionContributor"/>
<completion.contributor language="JavaScript" implementationClass="com.sdk.dynform.tools.i18n.I18nCompletionContributor"/>
<completion.contributor language="JAVA" implementationClass="com.sdk.dynform.tools.helper.DynFormCompletionContributor"/>
<completion.contributor language="XML" implementationClass="com.sdk.dynform.tools.helper.DynFormCompletionContributor"/>
<completion.contributor language="JavaScript" implementationClass="com.sdk.dynform.tools.helper.DynFormCompletionContributor"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1 @@
# Bundle for DynForm I18n Tools