feat: update plugin version to 3.2.9 and enhance i18n/reference support

- Extend I18n support for MESSAGE attribute in UNIQ-CHECK and other tags.
- Add code completion and reference navigation for CHECK-FIELDS resolving against related grid datasets.
- Fix duplicate i18n Inlay Hints in JSP and JS files by targeting the innermost PSI elements.
- Enhance MGET_PATTERN regex to support object-chained calls (e.g., factory.$M.get).
This commit is contained in:
2026-05-04 14:39:08 +07:00
parent 13ff47b7ae
commit b6dc46d775
10 changed files with 352 additions and 61 deletions

View File

@@ -0,0 +1,93 @@
<%@ page contentType="text/html; charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="sdk.json.*" %>
<%@ page import="sdk.utils.*" %>
<%@ page import="sdk.api.client.*" %>
<%@ page import="java.util.*" %>
<%@ page import="com.apps.SystemFactory" %>
<%@ page import="sdk.db.connector.*" %>
<%@ page import="com.bgt.model.bean.*" %>
<%!
SDKLogger logger = new SDKLogger("Operate-actions");
public boolean bgt0102010_add(SystemFactory factory, JSONObject jsData) {
if (!jsData.getString("-- some important field --").isBlank()) {
DBConnector dbConn = factory.appDatabase.getXConnector();
try {
{
REFER_CODE dsRefcode = new REFER_CODE(dbConn);
dsRefcode.append();
dsRefcode.setRFG_GRP("STG-ITEMS");
dsRefcode.setRFC_CODE(jsData.getString(REFER_CODE.RFC_CODE));
dsRefcode.setRFC_DESC(jsData.getString(REFER_CODE.RFC_DESC));
dsRefcode.setRFC_FLAG(jsData.getString(REFER_CODE.RFC_FLAG));
dsRefcode.execute();
}
{
REFER_GROUP dsRefGroup = new REFER_GROUP(dbConn);
dsRefGroup.insert();
dsRefGroup.setRFG_CODE(jsData.getString(REFER_CODE.RFC_CODE));
dsRefGroup.setRFG_NAME(jsData.getString(REFER_CODE.RFC_DESC));
dsRefGroup.setRFG_FLAG(jsData.getString(REFER_CODE.RFC_FLAG));
dsRefGroup.setRFG_EDITOR("STG");
dsRefGroup.execute();
}
factory.setRestCode("OK");
dbConn.close();
} catch (Exception ex) {
factory.setRestCode("ERROR");
factory.setRestMsg(ex.getMessage());
dbConn.close();
return false;
}
}
return true;
}
public boolean _action_skeleton(SystemFactory factory, JSONObject jsData) {
if (!jsData.getString("-- some important field --").isBlank()) {
DBConnector dbConn = factory.appDatabase.getXConnector();
try {
factory.setRestCode("OK");
dbConn.close();
} catch (Exception ex) {
factory.setRestCode("ERROR");
factory.setRestMsg(ex.getMessage());
dbConn.close();
return false;
}
}
return true;
}
public boolean execute(String action, SystemFactory factory) {
try {
Class<?> thisClass = getClass();
Method mtAction = thisClass.getMethod(action, SystemFactory.class, JSONObject.class);
String data = factory.rqsCtx.getParameter("data", "{}");
JSONObject jsData = new JSONObject(data);
boolean result = (boolean) mtAction.invoke(this, factory, jsData);
return result;
} catch (Exception e) {
factory.setRestCode("ERROR");
factory.setRestMsg(e+"\n"+JUtils.stackToString(e,10));
logger.error(e);
return false;
}
}
%>
<%
SystemFactory factory = SystemFactory.getInstance(request);
String action = factory.rqsCtx.getParameter("action", "");
if (factory.isValidJaxVF()) {
action = action.replaceAll("-", "_").replaceAll(" ", "_");
this.execute(action, factory);
} else {
factory.setRestCode("ERROR");
factory.setRestMsg("TK-Controller mismatch.");
}
%>

View File

@@ -4,7 +4,7 @@ plugins {
id("org.jetbrains.intellij.platform") version "2.7.0" id("org.jetbrains.intellij.platform") version "2.7.0"
} }
group = "com.sdk.dynform.tools" group = "com.sdk.dynform.tools"
version = "3.2.8" version = "3.2.9"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -38,7 +38,13 @@ intellijPlatform {
} }
changeNotes = """ changeNotes = """
<h2>[3.2.7]</h2> <h2>[3.2.9]</h2>
<ul>
<li><strong>Extended I18n Support:</strong> Added Inlay Hints, Autocomplete, and Reference support for the <code>MESSAGE</code> attribute in <code>&lt;UNIQ-CHECK&gt;</code> and other tags.</li>
<li><strong>Grid Field Validation:</strong> Introduced code completion and reference navigation for <code>CHECK-FIELDS</code> in <code>&lt;UNIQ-CHECK&gt;</code>, resolving against the related grid dataset.</li>
<li><strong>I18n Hint Stabilization:</strong> Resolved an issue causing duplicate Inlay Hints in JSP and JS files by targeting the innermost PSI elements and improving regex support for object-chained calls (e.g., <code>factory.${'$'}M.get</code>).</li>
</ul>
<h2>[3.2.8]</h2>
<ul> <ul>
<li><strong>Persistent Generation Context:</strong> Generator now remembers the last used directory for Action Beans and Dataset XMLs independently per project.</li> <li><strong>Persistent Generation Context:</strong> Generator now remembers the last used directory for Action Beans and Dataset XMLs independently per project.</li>
<li><strong>Configurable Auto-Open:</strong> Added new settings to toggle automatic file opening after generation and customize the table limit.</li> <li><strong>Configurable Auto-Open:</strong> Added new settings to toggle automatic file opening after generation and customize the table limit.</li>

View File

@@ -256,6 +256,32 @@ public class DynFormCompletionContributor extends CompletionContributor {
addFileIncludeCompletions(parameters, resultSet); addFileIncludeCompletions(parameters, resultSet);
} }
}); });
// XML completion for UNIQ-CHECK:CHECK-FIELDS
extend(CompletionType.BASIC, XmlPatterns.psiElement()
.inside(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("CHECK-FIELDS")
.withParent(XmlPatterns.xmlTag().withName("UNIQ-CHECK")))),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
XmlTag uniqCheckTag = PsiTreeUtil.getParentOfType(position, XmlTag.class);
if (uniqCheckTag == null) return;
XmlTag dataGridTag = uniqCheckTag.getParentTag();
if (dataGridTag != null && "DATA-GRID".equals(dataGridTag.getName())) {
// Split by separators to handle multi-key completion
String content = parameters.getPosition().getText().replace(CompletionUtil.DUMMY_IDENTIFIER_TRIMMED, "");
int lastSeparator = Math.max(content.lastIndexOf(','), content.lastIndexOf('+'));
String prefix = lastSeparator != -1 ? content.substring(lastSeparator + 1).trim() : content;
addFieldsFromGrid(dataGridTag, resultSet.withPrefixMatcher(prefix));
}
}
});
} }
private void addFieldsInTagRecursive(XmlTag container, @NotNull CompletionResultSet resultSet) { private void addFieldsInTagRecursive(XmlTag container, @NotNull CompletionResultSet resultSet) {

View File

@@ -143,6 +143,18 @@ public class DynFormPathUtils {
return null; return null;
} }
@Nullable
public static PsiFile findExecJsp(@NotNull PsiFile contextFile, @NotNull String name) {
VirtualFile moduleDir = findModuleDir(contextFile);
if (moduleDir != null) {
VirtualFile jspFile = moduleDir.findFileByRelativePath("exec/" + name + ".jsp");
if (jspFile != null) {
return PsiManager.getInstance(contextFile.getProject()).findFile(jspFile);
}
}
return null;
}
@NotNull @NotNull
public static List<PsiFile> getAllFrmlFiles(@NotNull PsiFile contextFile) { public static List<PsiFile> getAllFrmlFiles(@NotNull PsiFile contextFile) {
List<PsiFile> files = new ArrayList<>(); List<PsiFile> files = new ArrayList<>();

View File

@@ -203,6 +203,64 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
return new PsiReference[]{new DynFormFileIncludeReference(attrValue, new TextRange(1, value.length() + 1), value)}; return new PsiReference[]{new DynFormFileIncludeReference(attrValue, new TextRange(1, value.length() + 1), value)};
} }
}); });
// XML Reference for EXECUTOR NAME and ACTIONS
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("NAME", "ADD-ACTION", "UPDATE-ACTION", "DELETE-ACTION")
.withParent(XmlPatterns.xmlTag().withName("EXECUTOR"))),
new PsiReferenceProvider() {
@NotNull
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
XmlAttributeValue attrValue = (XmlAttributeValue) element;
String value = attrValue.getValue();
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
XmlAttribute attr = (XmlAttribute) attrValue.getParent();
String attrName = attr.getName();
if ("NAME".equals(attrName)) {
return new PsiReference[]{new DynFormExecutorNameReference(attrValue, new TextRange(1, value.length() + 1), value)};
} else {
return new PsiReference[]{new DynFormExecutorActionReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
}
});
// XML Reference for UNIQ-CHECK:CHECK-FIELDS
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("CHECK-FIELDS")
.withParent(XmlPatterns.xmlTag().withName("UNIQ-CHECK"))),
new PsiReferenceProvider() {
@NotNull
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
XmlAttributeValue attrValue = (XmlAttributeValue) element;
String value = attrValue.getValue();
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
List<PsiReference> refs = new ArrayList<>();
java.util.regex.Pattern sepPattern = java.util.regex.Pattern.compile("[,+]");
java.util.regex.Matcher matcher = sepPattern.matcher(value);
int lastEnd = 0;
while (matcher.find()) {
addUniqCheckRef(refs, attrValue, value, lastEnd, matcher.start());
lastEnd = matcher.end();
}
addUniqCheckRef(refs, attrValue, value, lastEnd, value.length());
return refs.toArray(new PsiReference[0]);
}
private void addUniqCheckRef(List<PsiReference> refs, XmlAttributeValue element, String content, int start, int end) {
String field = content.substring(start, end).trim();
if (!field.isEmpty()) {
int actualStart = content.indexOf(field, start);
refs.add(new DynFormUniqCheckFieldReference(element, new TextRange(actualStart + 1, actualStart + 1 + field.length()), field));
}
}
});
} }
private static boolean hasAncestorWithName(XmlTag tag, String name) { private static boolean hasAncestorWithName(XmlTag tag, String name) {
@@ -821,6 +879,111 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
} }
} }
private static class DynFormExecutorNameReference extends PsiReferenceBase<XmlAttributeValue> {
private final String executorName;
public DynFormExecutorNameReference(@NotNull XmlAttributeValue element, TextRange range, String executorName) {
super(element, range, true);
this.executorName = executorName;
}
@Nullable @Override public PsiElement resolve() {
return DynFormPathUtils.findExecJsp(myElement.getContainingFile(), executorName);
}
}
private static class DynFormExecutorActionReference extends PsiReferenceBase<XmlAttributeValue> {
private final String actionName;
public DynFormExecutorActionReference(@NotNull XmlAttributeValue element, TextRange range, String actionName) {
super(element, range, true);
this.actionName = actionName;
}
@Nullable @Override public PsiElement resolve() {
XmlTag executorTag = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
if (executorTag == null) return null;
String executorName = executorTag.getAttributeValue("NAME");
if (executorName == null) return null;
PsiFile jspFile = DynFormPathUtils.findExecJsp(myElement.getContainingFile(), executorName);
if (jspFile != null) {
return jspFile;
}
return null;
}
}
private static class DynFormUniqCheckFieldReference extends PsiReferenceBase<XmlAttributeValue> implements PsiPolyVariantReference {
private final String fieldName;
public DynFormUniqCheckFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
super(element, range, true);
this.fieldName = fieldName;
}
@Override
public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
List<ResolveResult> results = new ArrayList<>();
XmlAttribute attr = (XmlAttribute) myElement.getParent();
XmlTag uniqCheckTag = (XmlTag) attr.getParent();
XmlTag dataGridTag = uniqCheckTag.getParentTag();
if (dataGridTag != null && "DATA-GRID".equals(dataGridTag.getName())) {
PsiElement found = findFieldInGrid(dataGridTag);
if (found != null) results.add(new PsiElementResolveResult(found));
}
return filterOpenFiles(results, myElement.getProject());
}
@Nullable
@Override
public PsiElement resolve() {
ResolveResult[] results = multiResolve(false);
return results.length == 1 ? results[0].getElement() : null;
}
@Nullable
private PsiElement findFieldInGrid(XmlTag gridTag) {
String dataId = null;
XmlTag listTag = gridTag.findFirstSubTag("GRID-LIST");
if (listTag != null) dataId = listTag.getAttributeValue("DATAID");
if (dataId == null) {
XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR");
if (editorTag != null) dataId = editorTag.getAttributeValue("DATAID");
}
if (dataId != null && !dataId.isEmpty()) {
PsiFile file = gridTag.getContainingFile();
PsiElement datasetElement = findDatasetElement(file, dataId);
if (datasetElement instanceof XmlTag datasetTag) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
return findFieldInTag(fieldsTag, fieldName);
}
}
}
return null;
}
private PsiElement findDatasetElement(PsiFile file, String id) {
if (!(file instanceof XmlFile xmlFile)) return null;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null;
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
if (datasetsContainer == null) datasetsContainer = rootTag;
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
if (id.equals(datasetTag.getAttributeValue("ID"))) {
return datasetTag;
}
}
return null;
}
@Override
public Object @NotNull [] getVariants() {
return new Object[0];
}
}
private static ResolveResult[] filterOpenFiles(List<ResolveResult> results, Project project) { private static ResolveResult[] filterOpenFiles(List<ResolveResult> results, Project project) {
if (results.size() <= 1) return results.toArray(new ResolveResult[0]); if (results.size() <= 1) return results.toArray(new ResolveResult[0]);
FileEditorManager editorManager = FileEditorManager.getInstance(project); FileEditorManager editorManager = FileEditorManager.getInstance(project);

View File

@@ -44,7 +44,7 @@ public class I18nCompletionContributor extends CompletionContributor {
new com.intellij.openapi.util.TextRange(Math.max(0, parameters.getOffset() - 100), parameters.getOffset()) 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); java.util.regex.Matcher m = java.util.regex.Pattern.compile("(?:\\w+\\.)?\\$M\\.get\\([\"']([^\"']*)$").matcher(textBefore);
if (m.find()) { if (m.find()) {
handleMultiKeyCompletion(parameters, resultSet, m.group(1)); handleMultiKeyCompletion(parameters, resultSet, m.group(1));
} }
@@ -64,7 +64,7 @@ public class I18nCompletionContributor extends CompletionContributor {
XmlAttribute attribute = PsiTreeUtil.getParentOfType(position, XmlAttribute.class); XmlAttribute attribute = PsiTreeUtil.getParentOfType(position, XmlAttribute.class);
if (attribute != null) { if (attribute != null) {
String name = attribute.getName(); String name = attribute.getName();
if (name.equals("CAPTION") || name.equals("LABEL")) { if (name.equals("CAPTION") || name.equals("LABEL") || name.equals("MESSAGE")) {
XmlAttributeValue valueElement = attribute.getValueElement(); XmlAttributeValue valueElement = attribute.getValueElement();
if (valueElement != null) { if (valueElement != null) {
int offsetInValue = parameters.getOffset() - valueElement.getTextRange().getStartOffset() - 1; int offsetInValue = parameters.getOffset() - valueElement.getTextRange().getStartOffset() - 1;
@@ -152,7 +152,7 @@ public class I18nCompletionContributor extends CompletionContributor {
String methodName = methodCall.getMethodExpression().getReferenceName(); String methodName = methodCall.getMethodExpression().getReferenceName();
if ("get".equals(methodName)) { if ("get".equals(methodName)) {
PsiElement qualifier = methodCall.getMethodExpression().getQualifier(); PsiElement qualifier = methodCall.getMethodExpression().getQualifier();
return qualifier != null && "$M".equals(qualifier.getText()); return qualifier != null && (qualifier.getText().endsWith("$M") || "$M".equals(qualifier.getText()));
} }
} }
} }

View File

@@ -7,12 +7,13 @@ import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlToken;
import com.sdk.dynform.tools.config.DynFormSettings; import com.sdk.dynform.tools.config.DynFormSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.swing.*; import javax.swing.*;
import java.util.HashSet;
import java.util.Set;
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
public class I18nInlayHintsProvider implements InlayHintsProvider<NoSettings> { public class I18nInlayHintsProvider implements InlayHintsProvider<NoSettings> {
@@ -25,8 +26,7 @@ public class I18nInlayHintsProvider implements InlayHintsProvider<NoSettings> {
return null; return null;
} }
// Prevent duplicate hints: only provide hints if the language being queried // Prevent duplicate hints across multiple language trees in the same file
// matches the primary language of the file.
if (!file.getLanguage().equals(file.getViewProvider().getBaseLanguage())) { if (!file.getLanguage().equals(file.getViewProvider().getBaseLanguage())) {
return null; return null;
} }
@@ -35,6 +35,9 @@ public class I18nInlayHintsProvider implements InlayHintsProvider<NoSettings> {
} }
private static class I18nCollector extends FactoryInlayHintsCollector { private static class I18nCollector extends FactoryInlayHintsCollector {
// ใช้ Set เพื่อจำ offset ที่เคยแสดง Hint ไปแล้ว ป้องกันการแสดงผลซ้ำซ้อนในโหนดแม่/ลูก
private final Set<Integer> handledOffsets = new HashSet<>();
public I18nCollector(@NotNull Editor editor) { public I18nCollector(@NotNull Editor editor) {
super(editor); super(editor);
} }
@@ -42,58 +45,39 @@ public class I18nInlayHintsProvider implements InlayHintsProvider<NoSettings> {
@Override @Override
public boolean collect(@NotNull PsiElement element, @NotNull Editor editor, @NotNull InlayHintsSink sink) { public boolean collect(@NotNull PsiElement element, @NotNull Editor editor, @NotNull InlayHintsSink sink) {
String lang = element.getLanguage().getDisplayName().toLowerCase(); 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") // 1. Regex based hints ($M.get และ @M{})
boolean isJsOrJava = checkLangs.contains(lang) || (baseLang != null && checkLangs.contains(baseLang.getDisplayName().toLowerCase())); // ประมวลผลเมื่อเป็นไฟล์เต็มๆ (PsiFile) หรือ element ย่อยที่มีขนาดไม่ใหญ่เกินไปเพื่อป้องกัน O(N^2)
if (element instanceof PsiFile || element.getTextLength() < 5000) {
if (isJsOrJava) { String text = element.getText();
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) { if (text != null) {
// Handle @M{key} int baseOffset = element.getTextRange().getStartOffset();
if (text.contains("@M{")) {
java.util.regex.Matcher matcher = I18nUtils.getMPattern().matcher(text); // Handle $M.get("key")
while (matcher.find()) { java.util.regex.Matcher matcher1 = I18nUtils.getMGetPattern().matcher(text);
String key = matcher.group(1); while (matcher1.find()) {
if (key != null) { String key = matcher1.group(1);
if (key != null) {
int offset = baseOffset + matcher1.end();
if (handledOffsets.add(offset)) {
String translation = I18nUtils.findMessageValue(element.getProject(), key); String translation = I18nUtils.findMessageValue(element.getProject(), key);
if (translation != null) { if (translation != null) {
sink.addInlineElement(token.getTextRange().getStartOffset() + matcher.end(), true, sink.addInlineElement(offset, true, getFactory().text(" \u00AB " + translation + " \u00BB"), false);
getFactory().text(" \u00AB " + translation + " \u00BB"), false);
} }
} }
} }
} }
// Handle $M.get in XML (e.g. inside script tags or attributes) // Handle @M{key}
if (text.contains("$M.get")) { java.util.regex.Matcher matcher2 = I18nUtils.getMPattern().matcher(text);
java.util.regex.Matcher matcher = I18nUtils.getMGetPattern().matcher(text); while (matcher2.find()) {
while (matcher.find()) { String key = matcher2.group(1);
String key = matcher.group(1); if (key != null) {
if (key != null) { int offset = baseOffset + matcher2.end();
if (handledOffsets.add(offset)) {
String translation = I18nUtils.findMessageValue(element.getProject(), key); String translation = I18nUtils.findMessageValue(element.getProject(), key);
if (translation != null) { if (translation != null) {
sink.addInlineElement(token.getTextRange().getStartOffset() + matcher.end(), true, sink.addInlineElement(offset, true, getFactory().text(" \u00AB " + translation + " \u00BB"), false);
getFactory().text(" \u00AB " + translation + " \u00BB"), false);
} }
} }
} }
@@ -101,16 +85,18 @@ public class I18nInlayHintsProvider implements InlayHintsProvider<NoSettings> {
} }
} }
// 3. FRML XML CAPTION/LABEL // 2. FRML XML CAPTION/LABEL/MESSAGE
if ("xml#frml".contains(lang) && element instanceof XmlAttribute attribute) { if ("xml#frml".contains(lang) && element instanceof XmlAttribute attribute) {
String name = attribute.getName(); String name = attribute.getName();
if (name.equals("CAPTION") || name.equals("LABEL")) { if (name.equals("CAPTION") || name.equals("LABEL") || name.equals("MESSAGE")) {
String value = attribute.getValue(); String value = attribute.getValue();
if (value != null && !value.contains("@M{")) { if (value != null && !value.contains("@M{")) {
String translation = I18nUtils.findMessageValue(element.getProject(), value); int offset = attribute.getTextRange().getEndOffset();
if (translation != null) { if (handledOffsets.add(offset)) {
sink.addInlineElement(attribute.getTextRange().getEndOffset(), true, String translation = I18nUtils.findMessageValue(element.getProject(), value);
getFactory().text(" \u00AB " + translation + " \u00BB"), false); if (translation != null) {
sink.addInlineElement(offset, true, getFactory().text(" \u00AB " + translation + " \u00BB"), false);
}
} }
} }
} }

View File

@@ -32,8 +32,8 @@ public class I18nReferenceContributor extends PsiReferenceContributor {
} }
}); });
// 2. XML Attribute Reference Provider (for .frml CAPTION/LABEL) // 2. XML Attribute Reference Provider (for .frml CAPTION/LABEL/MESSAGE)
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("CAPTION", "LABEL")), registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName("CAPTION", "LABEL", "MESSAGE")),
new PsiReferenceProvider() { new PsiReferenceProvider() {
@NotNull @NotNull
@Override @Override
@@ -118,7 +118,6 @@ public class I18nReferenceContributor extends PsiReferenceContributor {
private void addMultiKeyReferences(List<PsiReference> refs, PsiElement element, String text, Pattern pattern) { private void addMultiKeyReferences(List<PsiReference> refs, PsiElement element, String text, Pattern pattern) {
Matcher mMatcher = pattern.matcher(text); Matcher mMatcher = pattern.matcher(text);
int elementStart = element.getTextRange().getStartOffset();
while (mMatcher.find()) { while (mMatcher.find()) {
if (mMatcher.groupCount() >= 1) { if (mMatcher.groupCount() >= 1) {
String content = mMatcher.group(1); String content = mMatcher.group(1);
@@ -154,7 +153,7 @@ public class I18nReferenceContributor extends PsiReferenceContributor {
String methodName = methodCall.getMethodExpression().getReferenceName(); String methodName = methodCall.getMethodExpression().getReferenceName();
if ("get".equals(methodName)) { if ("get".equals(methodName)) {
PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression();
return qualifier != null && "$M".equals(qualifier.getText()); return qualifier != null && (qualifier.getText().endsWith("$M") || "$M".equals(qualifier.getText()));
} }
} }
} }

View File

@@ -217,7 +217,7 @@ public class I18nUtils {
} }
private static final java.util.regex.Pattern M_PATTERN = java.util.regex.Pattern.compile("@M\\{(.*?)}"); 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*\\)"); private static final java.util.regex.Pattern MGET_PATTERN = java.util.regex.Pattern.compile("(?:[\\w.]+\\.)?\\$M\\.get\\(\\s*[\"'](.*?)[\"']\\s*\\)");
@NotNull @NotNull

View File

@@ -34,6 +34,12 @@
]]></description> ]]></description>
<change-notes><![CDATA[ <change-notes><![CDATA[
<h2>[3.2.9]</h2>
<ul>
<li><strong>Extended I18n Support:</strong> Added Inlay Hints, Autocomplete, and Reference support for the <code>MESSAGE</code> attribute in <code>&lt;UNIQ-CHECK&gt;</code> and other tags.</li>
<li><strong>Grid Field Validation:</strong> Introduced code completion and reference navigation for <code>CHECK-FIELDS</code> in <code>&lt;UNIQ-CHECK&gt;</code>, resolving against the related grid dataset.</li>
<li><strong>I18n Hint Stabilization:</strong> Resolved an issue causing duplicate Inlay Hints in JSP and JS files by targeting the innermost PSI elements and improving regex support for object-chained calls (e.g., <code>factory.$M.get</code>).</li>
</ul>
<h2>[3.2.8]</h2> <h2>[3.2.8]</h2>
<ul> <ul>
<li><strong>Cross-file Reference Resolution:</strong> Enhanced resolution of Datasets and Fields (e.g., DS-MASTER) to search upward across included `.frml` files.</li> <li><strong>Cross-file Reference Resolution:</strong> Enhanced resolution of Datasets and Fields (e.g., DS-MASTER) to search upward across included `.frml` files.</li>