feat: implement advanced bidirectional field referencing and cross-module path resolution

- Core Logic Enhancements:
    - Implement bidirectional field referencing between <FIELDS>, <LAYOUT>, and <TITLES> tags in .frml files, enabling seamless navigation from definitions to usages and vice versa.
    - Add robust support for AJAX-OPTION field mapping:
        - SRC attribute: Links to field definitions within defs/ajax.xml datasets.
        - TARGET attribute: Links to local field definitions within the same form.
    - Implement global grid resolution: GRID-ID now searches across the current file and all recursively included files (<INCLUDE>).
    - Enhance deep recursive search for fields/sections within nested tags like <SECTION>, <ROW>, and <FIELD-LIST>.

- Path Resolution & Helpers (DynFormPathUtils):
    - Added support for module-relative paths starting with # (mapping to view/frm/).
    - Added support for cross-module paths starting with / (mapping to WEB-INF/app/module/{module}/view/frm/).
    - Implemented auto-correction for common keyboard typos (Thai 'ิ' instead of /).
    - Added specialized helpers for locating ajax.xml and included files within the framework's structure.

- Smart Completion Enhancements:
    - Added context-aware completion for TARGET and SRC fields in AJAX update-fields.
    - Enabled global GRID-ID completion by scanning all included resources.
    - Improved dataset completion to include both local and AJAX-defined datasets.

- Test Resources:
    - Added a comprehensive set of real-world examples (bdgt04, bdgt05, bdgt06) in DevResources/full-examples/ to validate complex cross-module and master-detail scenarios.
This commit is contained in:
2026-04-10 12:56:04 +07:00
parent da049ea016
commit f705cd11b9
99 changed files with 20379 additions and 30 deletions

View File

@@ -3,15 +3,22 @@ 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.patterns.XmlPatterns;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class DynFormCompletionContributor extends CompletionContributor {
public DynFormCompletionContributor() {
// Java completion
extend(CompletionType.BASIC, PlatformPatterns.psiElement().withParent(PsiLiteralExpression.class),
new CompletionProvider<CompletionParameters>() {
@Override
@@ -27,7 +34,6 @@ public class DynFormCompletionContributor extends CompletionContributor {
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) {
@@ -35,7 +41,6 @@ public class DynFormCompletionContributor extends CompletionContributor {
.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) {
@@ -50,6 +55,191 @@ public class DynFormCompletionContributor extends CompletionContributor {
}
}
});
// XML completion for Field/Section NAME/ID/TARGET
extend(CompletionType.BASIC, XmlPatterns.psiElement()
.inside(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("NAME", "ID", "TARGET")
.withParent(XmlPatterns.xmlTag().withName("FIELD", "SECTION")))),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
XmlAttribute attr = PsiTreeUtil.getParentOfType(position, XmlAttribute.class);
if (attr == null) return;
if ("TARGET".equals(attr.getName())) {
// เสนอฟิลด์ในฟอร์มปัจจุบัน
XmlTag formContainer = PsiTreeUtil.getParentOfType(position, XmlTag.class);
while (formContainer != null && !"FORM_ENTRY".equals(formContainer.getName()) && !"FORM_BROWSE".equals(formContainer.getName())) {
formContainer = formContainer.getParentTag();
}
if (formContainer == null) {
XmlFile file = (XmlFile) parameters.getOriginalFile();
formContainer = file.getRootTag();
}
if (formContainer != null) {
addFieldsInTagRecursive(formContainer, resultSet);
}
} else {
// เสนอฟิลด์ภายใต้ LAYOUT container เดียวกัน
XmlTag layoutTag = PsiTreeUtil.getParentOfType(position, XmlTag.class);
while (layoutTag != null && !"LAYOUT".equals(layoutTag.getName())) {
layoutTag = layoutTag.getParentTag();
}
if (layoutTag == null) return;
XmlTag containerTag = layoutTag.getParentTag();
if (containerTag == null) return;
XmlTag fieldsTag = containerTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
}
});
// XML completion for Dataset ID
extend(CompletionType.BASIC, XmlPatterns.psiElement()
.inside(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("DATAID", "DATASET", "DATASET-ID"))),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
addDatasetsInFile(parameters.getOriginalFile(), resultSet);
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(parameters.getOriginalFile());
if (ajaxFile != null) {
addDatasetsInFile(ajaxFile, resultSet);
}
}
});
// XML completion for SRC in UPDATE-FIELDS
extend(CompletionType.BASIC, XmlPatterns.psiElement()
.inside(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("SRC")
.withParent(XmlPatterns.xmlTag().withName("FIELD")
.withParent(XmlPatterns.xmlTag().withName("UPDATE-FIELDS"))))),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
XmlTag ajaxOptionTag = PsiTreeUtil.getParentOfType(position, XmlTag.class);
while (ajaxOptionTag != null && !"AJAX-OPTION".equals(ajaxOptionTag.getName())) {
ajaxOptionTag = ajaxOptionTag.getParentTag();
}
if (ajaxOptionTag == null) return;
String datasetId = ajaxOptionTag.getAttributeValue("DATASET");
if (datasetId == null) return;
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(parameters.getOriginalFile());
if (ajaxFile instanceof XmlFile xmlFile) {
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag != null) {
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
if (datasetsContainer == null) datasetsContainer = rootTag;
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
if (datasetId.equals(datasetTag.getAttributeValue("ID"))) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
}
}
}
}
});
// XML completion for GRID-ID
extend(CompletionType.BASIC, XmlPatterns.psiElement()
.inside(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("GRID-ID"))),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
addGridsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>());
}
});
}
private void addFieldsInTagRecursive(XmlTag container, @NotNull CompletionResultSet resultSet) {
for (XmlTag subTag : container.getSubTags()) {
if ("FIELD".equals(subTag.getName())) {
String name = subTag.getAttributeValue("NAME");
if (name != null && !name.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(name)
.withIcon(com.intellij.icons.AllIcons.Nodes.Field)
.withTypeText(subTag.getAttributeValue("TYPE")));
}
} else {
if (("SECTION".equals(subTag.getName()) || "FIELDS".equals(subTag.getName())) && subTag.getAttributeValue("ID") != null) {
String id = subTag.getAttributeValue("ID");
resultSet.addElement(LookupElementBuilder.create(id)
.withIcon(com.intellij.icons.AllIcons.Nodes.Package)
.withItemTextForeground(java.awt.Color.BLUE));
}
addFieldsInTagRecursive(subTag, resultSet);
}
}
}
private void addDatasetsInFile(PsiFile file, @NotNull CompletionResultSet resultSet) {
if (!(file instanceof XmlFile xmlFile)) return;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return;
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
if (datasetsContainer == null) datasetsContainer = rootTag;
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
String id = datasetTag.getAttributeValue("ID");
if (id != null && !id.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(id)
.withIcon(com.intellij.icons.AllIcons.Nodes.DataTables)
.withTypeText(datasetTag.getAttributeValue("TABLENAME")));
}
}
}
private void addGridsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited) {
if (file == null || !visited.add(file)) return;
if (!(file instanceof XmlFile xmlFile)) return;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return;
XmlTag dataGridsTag = rootTag.findFirstSubTag("DATA-GRIDS");
if (dataGridsTag != null) {
for (XmlTag gridTag : dataGridsTag.findSubTags("DATA-GRID")) {
String id = gridTag.getAttributeValue("ID");
if (id != null && !id.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(id)
.withIcon(com.intellij.icons.AllIcons.Nodes.DataTables));
}
}
}
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
addGridsInFileRecursive(includedFile, resultSet, visited);
}
}
}
}
private PsiNewExpression getNewDynFormExpression(PsiLiteralExpression literal) {

View File

@@ -5,13 +5,10 @@ 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 {
@@ -20,12 +17,75 @@ public class DynFormPathUtils {
@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);
}
@Nullable
public static VirtualFile findModuleDir(@NotNull PsiFile file) {
VirtualFile vFile = file.getVirtualFile();
if (vFile == null) return null;
VirtualFile current = vFile.getParent();
while (current != null) {
if (current.getParent() != null && "module".equals(current.getParent().getName())) {
return current;
}
current = current.getParent();
}
return null;
}
@Nullable
public static PsiFile findAjaxXml(@NotNull PsiFile file) {
VirtualFile moduleDir = findModuleDir(file);
if (moduleDir != null) {
VirtualFile ajaxFile = moduleDir.findFileByRelativePath("defs/ajax.xml");
if (ajaxFile != null) {
return PsiManager.getInstance(file.getProject()).findFile(ajaxFile);
}
}
return null;
}
@Nullable
public static PsiFile findIncludedFile(@NotNull PsiFile contextFile, @NotNull String path) {
// จัดการกรณี typo (ิ แทน /)
path = path.replace("", "/");
VirtualFile moduleBase = getModuleBaseDir(contextFile.getProject());
if (moduleBase == null) return null;
VirtualFile targetVFile = null;
if (path.startsWith("#")) {
// ภายในโมดูลเดียวกัน: #grids/file.frml -> currentModule/view/frm/grids/file.frml
VirtualFile moduleDir = findModuleDir(contextFile);
if (moduleDir != null) {
String relativePath = "view/frm/" + path.substring(1);
targetVFile = moduleDir.findFileByRelativePath(relativePath);
}
} else if (path.startsWith("/")) {
// ข้ามโมดูล: /bdgt04/grids/file.frml -> module/bdgt04/view/frm/grids/file.frml
String cleanPath = path.substring(1);
int firstSlash = cleanPath.indexOf("/");
if (firstSlash > 0) {
String targetModule = cleanPath.substring(0, firstSlash);
String subPath = cleanPath.substring(firstSlash + 1);
String fullPath = targetModule + "/view/frm/" + subPath;
targetVFile = moduleBase.findFileByRelativePath(fullPath);
}
} else {
// สัมพัทธ์กับไฟล์ปัจจุบัน
targetVFile = contextFile.getVirtualFile().getParent().findFileByRelativePath(path);
}
if (targetVFile != null) {
return PsiManager.getInstance(contextFile.getProject()).findFile(targetVFile);
}
return null;
}
@NotNull
public static List<String> getAllModules(@NotNull Project project) {
List<String> modules = new ArrayList<>();

View File

@@ -2,14 +2,24 @@ package com.sdk.dynform.tools.helper;
import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.XmlPatterns;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
public class DynFormReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
// Java Reference Provider
registrar.registerReferenceProvider(PlatformPatterns.psiElement(PsiLiteralExpression.class),
new PsiReferenceProvider() {
@NotNull
@@ -24,11 +34,9 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
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) {
@@ -40,6 +48,109 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
return PsiReference.EMPTY_ARRAY;
}
});
// XML Reference Provider for Field/Section NAME/ID
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("NAME", "ID", "TARGET")
.withParent(XmlPatterns.xmlTag().withName("FIELD", "SECTION"))),
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();
XmlTag tag = (XmlTag) attr.getParent();
String attrName = attr.getName();
if ("TARGET".equals(attrName)) {
return new PsiReference[]{new DynFormLocalFieldReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
// เคส 1: อยู่ใน LAYOUT หรือ TITLES -> ลิงก์ไปหา FIELDS (นิยาม)
if (hasAncestorWithName(tag, "LAYOUT") || hasAncestorWithName(tag, "TITLES")) {
return new PsiReference[]{new DynFormFieldDefinitionReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
// เคส 2: อยู่ใน FIELDS (นิยาม) -> ลิงก์กลับไปหา LAYOUT หรือ TITLES (การใช้งาน)
if (hasAncestorWithName(tag, "FIELDS")) {
return new PsiReference[]{new DynFormFieldUsageReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
return PsiReference.EMPTY_ARRAY;
}
});
// XML Reference for Dataset ID (DATAID, DATASET, DATASET-ID)
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("DATAID", "DATASET", "DATASET-ID")),
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;
return new PsiReference[]{new DynFormDatasetReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
});
// XML Reference for Grid ID
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("GRID-ID")),
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;
return new PsiReference[]{new DynFormGridReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
});
// XML Reference for SRC in UPDATE-FIELDS
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("SRC")
.withParent(XmlPatterns.xmlTag().withName("FIELD")
.withParent(XmlPatterns.xmlTag().withName("UPDATE-FIELDS")))),
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;
return new PsiReference[]{new DynFormAjaxSrcFieldReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
});
// XML Reference for Include File
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("FILE")
.withParent(XmlPatterns.xmlTag().withName("INCLUDE")
.withParent(XmlPatterns.xmlTag().withName("INCLUDES")))),
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;
return new PsiReference[]{new DynFormFileIncludeReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
});
}
private static boolean hasAncestorWithName(XmlTag tag, String name) {
XmlTag current = tag.getParentTag();
while (current != null) {
if (name.equals(current.getName())) return true;
current = current.getParentTag();
}
return false;
}
@Nullable
@@ -68,15 +179,11 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
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() {
@Nullable @Override public PsiElement resolve() {
return DynFormPathUtils.findModuleDirectory(myElement.getProject(), moduleName);
}
}
@@ -84,17 +191,256 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
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() {
@Nullable @Override public PsiElement resolve() {
return DynFormPathUtils.findFrmlFile(myElement.getProject(), moduleName, frmlName);
}
}
private static class DynFormLocalFieldReference extends PsiReferenceBase<XmlAttributeValue> {
private final String fieldName;
public DynFormLocalFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
super(element, range);
this.fieldName = fieldName;
}
@Nullable @Override public PsiElement resolve() {
XmlTag container = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
while (container != null && !"FORM_ENTRY".equals(container.getName()) && !"FORM_BROWSE".equals(container.getName()) && !"GRID-LIST".equals(container.getName())) {
container = container.getParentTag();
}
if (container == null) {
XmlFile file = (XmlFile) myElement.getContainingFile();
container = file.getRootTag();
}
if (container == null) return null;
return findFieldInTag(container, fieldName);
}
}
/**
* ลิงก์จาก Layout หรือ Titles ไปหา FIELDS
*/
private static class DynFormFieldDefinitionReference extends PsiReferenceBase<XmlAttributeValue> {
private final String fieldName;
public DynFormFieldDefinitionReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
super(element, range);
this.fieldName = fieldName;
}
@Nullable @Override public PsiElement resolve() {
XmlTag tag = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
if (tag == null) return null;
// หา container (FORM_ENTRY, FORM_BROWSE, GRID-LIST หรือ FILTERS)
XmlTag container = tag;
while (container != null &&
!"LAYOUT".equals(container.getName()) &&
!"TITLES".equals(container.getName()) &&
!"FORM_ENTRY".equals(container.getName()) &&
!"FORM_BROWSE".equals(container.getName()) &&
!"GRID-LIST".equals(container.getName()) &&
!"FILTERS".equals(container.getName())) {
container = container.getParentTag();
}
if (container != null && ("LAYOUT".equals(container.getName()) || "TITLES".equals(container.getName()))) {
container = container.getParentTag();
}
if (container == null) return null;
XmlTag fieldsTag = container.findFirstSubTag("FIELDS");
if (fieldsTag == null) return null;
return findFieldInTag(fieldsTag, fieldName);
}
}
/**
* ค้นหาย้อนกลับจากนิยามฟิลด์ (FIELDS) ไปยังการใช้งานใน LAYOUT หรือ TITLES
*/
private static class DynFormFieldUsageReference extends PsiReferenceBase<XmlAttributeValue> {
private final String fieldName;
public DynFormFieldUsageReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
super(element, range);
this.fieldName = fieldName;
}
@Nullable @Override public PsiElement resolve() {
XmlTag tag = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
if (tag == null) return null;
// หา FIELDS container
XmlTag fieldsTag = tag;
while (fieldsTag != null && !"FIELDS".equals(fieldsTag.getName())) {
fieldsTag = fieldsTag.getParentTag();
}
if (fieldsTag == null) return null;
XmlTag containerTag = fieldsTag.getParentTag();
if (containerTag == null) return null;
// หาใน LAYOUT ก่อน
XmlTag layoutTag = containerTag.findFirstSubTag("LAYOUT");
if (layoutTag != null) {
PsiElement found = findFieldInTag(layoutTag, fieldName);
if (found != null) return found;
}
// ถ้าไม่เจอใน LAYOUT ให้หาใน TITLES
XmlTag titlesTag = containerTag.findFirstSubTag("TITLES");
if (titlesTag != null) {
PsiElement found = findFieldInTag(titlesTag, fieldName);
if (found != null) return found;
}
return null;
}
}
@Nullable
private static PsiElement findFieldInTag(XmlTag container, String name) {
for (XmlTag subTag : container.getSubTags()) {
if ("FIELD".equals(subTag.getName()) && name.equals(subTag.getAttributeValue("NAME"))) {
XmlAttribute nameAttr = subTag.getAttribute("NAME");
return nameAttr != null ? nameAttr.getValueElement() : subTag;
}
if (("SECTION".equals(subTag.getName()) || "ROW".equals(subTag.getName())) && name.equals(subTag.getAttributeValue("ID"))) {
XmlAttribute idAttr = subTag.getAttribute("ID");
return idAttr != null ? idAttr.getValueElement() : subTag;
}
PsiElement found = findFieldInTag(subTag, name);
if (found != null) return found;
}
return null;
}
private static class DynFormDatasetReference extends PsiReferenceBase<XmlAttributeValue> {
private final String datasetId;
public DynFormDatasetReference(@NotNull XmlAttributeValue element, TextRange range, String datasetId) {
super(element, range);
this.datasetId = datasetId;
}
@Nullable @Override public PsiElement resolve() {
PsiElement found = findDatasetInFile(myElement.getContainingFile(), datasetId);
if (found != null) return found;
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(myElement.getContainingFile());
if (ajaxFile != null) {
return findDatasetInFile(ajaxFile, datasetId);
}
return null;
}
@Nullable
private PsiElement findDatasetInFile(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"))) {
XmlAttribute idAttr = datasetTag.getAttribute("ID");
return idAttr != null ? idAttr.getValueElement() : datasetTag;
}
}
return null;
}
}
private static class DynFormGridReference extends PsiReferenceBase<XmlAttributeValue> {
private final String gridId;
public DynFormGridReference(@NotNull XmlAttributeValue element, TextRange range, String gridId) {
super(element, range);
this.gridId = gridId;
}
@Nullable @Override public PsiElement resolve() {
return findGridInFileRecursive(myElement.getContainingFile(), gridId, new HashSet<>());
}
@Nullable
private PsiElement findGridInFileRecursive(PsiFile file, String id, Set<PsiFile> visited) {
if (file == null || !visited.add(file)) return null;
if (!(file instanceof XmlFile xmlFile)) return null;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null;
XmlTag dataGridsTag = rootTag.findFirstSubTag("DATA-GRIDS");
if (dataGridsTag != null) {
for (XmlTag gridTag : dataGridsTag.findSubTags("DATA-GRID")) {
if (id.equals(gridTag.getAttributeValue("ID"))) {
XmlAttribute idAttr = gridTag.getAttribute("ID");
return idAttr != null ? idAttr.getValueElement() : gridTag;
}
}
}
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
PsiElement found = findGridInFileRecursive(includedFile, id, visited);
if (found != null) return found;
}
}
}
return null;
}
}
private static class DynFormAjaxSrcFieldReference extends PsiReferenceBase<XmlAttributeValue> {
private final String fieldName;
public DynFormAjaxSrcFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
super(element, range);
this.fieldName = fieldName;
}
@Nullable @Override public PsiElement resolve() {
XmlTag ajaxOptionTag = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
while (ajaxOptionTag != null && !"AJAX-OPTION".equals(ajaxOptionTag.getName())) {
ajaxOptionTag = ajaxOptionTag.getParentTag();
}
if (ajaxOptionTag == null) return null;
String datasetId = ajaxOptionTag.getAttributeValue("DATASET");
if (datasetId == null) return null;
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(myElement.getContainingFile());
if (!(ajaxFile 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 (datasetId.equals(datasetTag.getAttributeValue("ID"))) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
return findFieldInTag(fieldsTag, fieldName);
}
return datasetTag;
}
}
return null;
}
}
private static class DynFormFileIncludeReference extends PsiReferenceBase<XmlAttributeValue> {
private final String path;
public DynFormFileIncludeReference(@NotNull XmlAttributeValue element, TextRange range, String path) {
super(element, range);
this.path = path;
}
@Nullable @Override public PsiElement resolve() {
return DynFormPathUtils.findIncludedFile(myElement.getContainingFile(), path);
}
}
}