feat: comprehensive cross-file support and performance optimization (v3.3.0)

- Implemented cross-file completion, references, and validation for .frml files.
- Optimized resource discovery using IntelliJ indexing (ReferencesSearch) to fix IDE freeze.
- Refactored shared search logic into DynFormPathUtils.
- Excluded <ROW> tags from field definition requirements.
- Updated plugin version to 3.3.0.
This commit is contained in:
2026-05-14 18:27:57 +07:00
parent b6dc46d775
commit 431e51079c
35 changed files with 4693 additions and 565 deletions

View File

@@ -9,71 +9,47 @@ import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import com.intellij.psi.PsiFile;
import java.util.HashSet;
public class DynFormAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (!(element instanceof XmlAttributeValue attrValue)) return;
PsiElement parent = attrValue.getParent();
if (!(parent instanceof XmlAttribute attr)) return;
if (!"NAME".equals(attr.getName())) return;
PsiElement tagElem = attr.getParent();
if (!(tagElem instanceof XmlTag tag) || !"FIELD".equals(tag.getName())) return;
// Check if this FIELD is inside a LAYOUT tag
String attrName = attr.getName();
PsiElement tagElem = attr.getParent();
if (!(tagElem instanceof XmlTag tag)) return;
String tagName = tag.getName();
boolean isField = "FIELD".equals(tagName) && "NAME".equals(attrName);
boolean isContainer = ("SECTION".equals(tagName) || "SECTIONS".equals(tagName)) && "ID".equals(attrName);
if (!isField && !isContainer) return;
// Check if this tag is inside a LAYOUT tag
XmlTag layoutTag = tag.getParentTag();
while (layoutTag != null && !"LAYOUT".equals(layoutTag.getName())) {
layoutTag = layoutTag.getParentTag();
}
if (layoutTag == null) return;
// The "Owner" of the LAYOUT (e.g., FORM_ENTRY, FILTERS, GRID-EDITOR)
// The "Owner" of the LAYOUT (e.g., FORM_ENTRY, FILTERS, GRID-EDITOR, FORM_BROWSE, GRID-LIST)
XmlTag ownerTag = layoutTag.getParentTag();
if (ownerTag == null) return;
String fieldName = attrValue.getValue();
if (fieldName == null || fieldName.isEmpty()) return;
// Look for the FIELDS tag that is a sibling of the current LAYOUT
XmlTag fieldsTag = ownerTag.findFirstSubTag("FIELDS");
if (fieldsTag == null) {
holder.newAnnotation(HighlightSeverity.ERROR, "No <FIELDS> definition found in <" + ownerTag.getName() + ">")
.range(attrValue.getTextRange())
.create();
return;
}
if (!isFieldDefined(fieldsTag, fieldName)) {
holder.newAnnotation(HighlightSeverity.ERROR, "Field '" + fieldName + "' is not defined in <FIELDS> of <" + ownerTag.getName() + ">")
PsiFile currentFile = tag.getContainingFile();
if (DynFormPathUtils.findFieldInFormContext(currentFile, ownerTag.getName(), fieldName, new HashSet<>()) == null) {
holder.newAnnotation(HighlightSeverity.ERROR, (isField ? "Field '" : "Section/Row '") + fieldName + "' is not defined in <FIELDS> of <" + ownerTag.getName() + ">")
.range(attrValue.getTextRange())
.create();
}
}
private boolean isFieldDefined(XmlTag fieldsTag, String name) {
for (XmlTag field : fieldsTag.findSubTags("FIELD")) {
if (name.equals(field.getAttributeValue("NAME"))) {
return true;
}
}
// Also check inside SECTIONS or ROWS if any
for (XmlTag sub : fieldsTag.getSubTags()) {
if ("SECTION".equals(sub.getName()) || "ROW".equals(sub.getName())) {
if (isFieldDefined(sub, name)) return true;
}
}
return false;
}
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;
}
}

View File

@@ -85,10 +85,34 @@ public class DynFormCompletionContributor extends CompletionContributor {
}
if (formContainer != null) {
addFieldsInTagRecursive(formContainer, resultSet);
DynFormPathUtils.addFieldsInTagRecursive(formContainer, resultSet);
}
// เพิ่มเติม: เสนอฟิลด์จากไฟล์ที่ include ไฟล์นี้ (Includers)
List<PsiFile> includers = DynFormPathUtils.findIncluders(parameters.getOriginalFile());
for (PsiFile includer : includers) {
if (includer instanceof XmlFile xmlFile) {
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag != null) {
for (XmlTag formTag : rootTag.findSubTags("FORM")) {
for (XmlTag entryTag : formTag.findSubTags("FORM_ENTRY")) {
XmlTag fieldsTag = entryTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
DynFormPathUtils.addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
for (XmlTag browseTag : formTag.findSubTags("FORM_BROWSE")) {
XmlTag fieldsTag = browseTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
DynFormPathUtils.addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
}
}
}
}
} else {
// เสนอฟิลด์ภายใต้ LAYOUT container เดียวกัน
// เสนอฟิลด์ภายใต้ LAYOUT container เดียวกัน (ข้ามไฟล์ได้)
XmlTag layoutTag = PsiTreeUtil.getParentOfType(position, XmlTag.class);
while (layoutTag != null && !"LAYOUT".equals(layoutTag.getName())) {
layoutTag = layoutTag.getParentTag();
@@ -96,10 +120,8 @@ public class DynFormCompletionContributor extends CompletionContributor {
if (layoutTag == null) return;
XmlTag containerTag = layoutTag.getParentTag();
if (containerTag == null) return;
XmlTag fieldsTag = containerTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
}
DynFormPathUtils.addFieldsInFormContext(parameters.getOriginalFile(), containerTag.getName(), resultSet, new HashSet<>());
}
}
});
@@ -120,10 +142,10 @@ public class DynFormCompletionContributor extends CompletionContributor {
if (isAjaxOption) {
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(parameters.getOriginalFile());
if (ajaxFile != null) {
addDatasetsInFile(ajaxFile, resultSet);
DynFormPathUtils.addDatasetsInFile(ajaxFile, resultSet);
}
} else {
addDatasetsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>(), false);
DynFormPathUtils.addDatasetsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>(), false);
}
}
});
@@ -164,7 +186,7 @@ public class DynFormCompletionContributor extends CompletionContributor {
if (parentDataset != null && "DATASET".equals(parentDataset.getName())) {
XmlTag fieldsTag = parentDataset.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
DynFormPathUtils.addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
}
@@ -205,7 +227,7 @@ public class DynFormCompletionContributor extends CompletionContributor {
if (datasetId.equals(datasetTag.getAttributeValue("ID"))) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
DynFormPathUtils.addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
}
@@ -223,7 +245,7 @@ public class DynFormCompletionContributor extends CompletionContributor {
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
addGridsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>());
DynFormPathUtils.addGridsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>());
}
});
@@ -239,7 +261,7 @@ public class DynFormCompletionContributor extends CompletionContributor {
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
addFormFieldsForNameRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>());
DynFormPathUtils.addFormFieldsForNameRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>());
}
});
@@ -284,171 +306,16 @@ public class DynFormCompletionContributor extends CompletionContributor {
});
}
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 addDatasetsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited, boolean includeAjax) {
if (file == null || !visited.add(file)) return;
if (includeAjax) {
// สำหรับ AJAX-OPTION ให้หาใน ajax.xml เท่านั้น
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file);
if (ajaxFile != null) {
addDatasetsInFile(ajaxFile, resultSet);
}
return;
}
// 1. Add datasets from current file
addDatasetsInFile(file, resultSet);
// 2. Add datasets from files that INCLUDE this file (Parent/Main Files)
if (visited.size() == 1) {
List<PsiFile> includers = DynFormPathUtils.findIncluders(file);
for (PsiFile includer : includers) {
if (visited.add(includer)) {
addDatasetsInFile(includer, resultSet);
}
}
}
// 3. Add datasets from included files (Downward)
if (file instanceof XmlFile xmlFile) {
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag != null) {
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);
if (includedFile != null && !visited.contains(includedFile)) {
addDatasetsInFileRecursive(includedFile, resultSet, visited, includeAjax);
}
}
}
}
}
}
// 4. Add datasets from all .frml files in module (fallback)
if (visited.size() <= 2) {
for (PsiFile frmlFile : DynFormPathUtils.getAllFrmlFiles(file)) {
if (visited.add(frmlFile)) {
addDatasetsInFile(frmlFile, 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;
// 1. Add grids from current file
addGridsInFile(file, resultSet);
// 2. Add grids from files that INCLUDE this file (Parent/Main Files)
if (visited.size() == 1) {
List<PsiFile> includers = DynFormPathUtils.findIncluders(file);
for (PsiFile includer : includers) {
if (visited.add(includer)) {
addGridsInFile(includer, resultSet);
}
}
}
// 3. Add grids from included files (Downward)
if (file instanceof XmlFile xmlFile) {
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag != null) {
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);
if (includedFile != null && !visited.contains(includedFile)) {
addGridsInFileRecursive(includedFile, resultSet, visited);
}
}
}
}
}
}
// 4. Add grids from all .frml files in module (fallback)
if (visited.size() <= 2) {
for (PsiFile frmlFile : DynFormPathUtils.getAllFrmlFiles(file)) {
if (visited.add(frmlFile)) {
addGridsInFile(frmlFile, resultSet);
}
}
}
}
private void addGridsInFile(PsiFile file, @NotNull CompletionResultSet resultSet) {
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));
}
}
}
}
private void addFieldsFromDatasetId(XmlTag tag, @NotNull CompletionResultSet resultSet) {
String datasetId = tag.getAttributeValue("DATASET-ID");
if (datasetId == null || datasetId.isEmpty()) return;
PsiFile file = tag.getContainingFile();
PsiElement datasetElement = findDatasetElement(file, datasetId);
PsiElement datasetElement = DynFormPathUtils.findDatasetElement(file, datasetId);
if (datasetElement instanceof XmlTag datasetTag) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
DynFormPathUtils.addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
}
@@ -469,11 +336,11 @@ public class DynFormCompletionContributor extends CompletionContributor {
// 2. Add fields from that DATASET
if (dataId != null && !dataId.isEmpty()) {
PsiElement datasetElement = findDatasetElement(gridTag.getContainingFile(), dataId);
PsiElement datasetElement = DynFormPathUtils.findDatasetElement(gridTag.getContainingFile(), dataId);
if (datasetElement instanceof XmlTag datasetTag) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
DynFormPathUtils.addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
}
@@ -482,98 +349,18 @@ public class DynFormCompletionContributor extends CompletionContributor {
if (listTag != null) {
XmlTag fieldsTag = listTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
DynFormPathUtils.addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR");
if (editorTag != null) {
XmlTag fieldsTag = editorTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
DynFormPathUtils.addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
}
private PsiElement findDatasetElement(PsiFile file, String id) {
Set<PsiFile> visited = new HashSet<>();
// 1. Local
PsiElement found = findDatasetInFile(file, id);
if (found != null) return found;
visited.add(file);
// 2. Includers
List<PsiFile> includers = DynFormPathUtils.findIncluders(file);
for (PsiFile includer : includers) {
found = findDatasetInFile(includer, id);
if (found != null) return found;
visited.add(includer);
}
// 3. Ajax
PsiFile ajaxXml = DynFormPathUtils.findAjaxXml(file);
if (ajaxXml != null && visited.add(ajaxXml)) {
found = findDatasetInFile(ajaxXml, id);
if (found != null) return found;
}
// 4. Recursive search
return findDatasetInFileRecursiveForCompletion(file, id, visited);
}
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"))) {
return datasetTag;
}
}
return null;
}
private PsiElement findDatasetInFileRecursiveForCompletion(PsiFile file, String id, Set<PsiFile> visited) {
if (file == null || !visited.add(file)) return null;
if (!(file instanceof XmlFile xmlFile)) return null;
// Try local (should already be checked by findDatasetElement but for recursion consistency)
PsiElement found = findDatasetInFile(xmlFile, id);
if (found != null) return found;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null;
// Downward search
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);
found = findDatasetInFileRecursiveForCompletion(includedFile, id, visited);
if (found != null) return found;
}
}
}
// Final fallback: Module-wide
if (visited.size() <= 2) {
for (PsiFile frmlFile : DynFormPathUtils.getAllFrmlFiles(file)) {
if (!visited.contains(frmlFile)) {
found = findDatasetInFile(frmlFile, id);
if (found != null) return found;
}
}
}
return null;
}
private static boolean hasAncestorWithName(XmlTag tag, String name) {
XmlTag current = tag.getParentTag();
while (current != null) {
@@ -676,60 +463,4 @@ public class DynFormCompletionContributor extends CompletionContributor {
}
}
}
private void addFormFieldsForNameRecursive(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;
// 1. Search in current file FORM_ENTRY tags
for (XmlTag formTag : rootTag.findSubTags("FORM")) {
for (XmlTag entryTag : formTag.findSubTags("FORM_ENTRY")) {
// Collect hidden fields from FIELDS
XmlTag fieldsTag = entryTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
for (XmlTag fieldTag : fieldsTag.findSubTags("FIELD")) {
if ("HIDDEN".equals(fieldTag.getAttributeValue("INPUTTYPE"))) {
String name = fieldTag.getAttributeValue("NAME");
if (name != null && !name.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(name)
.withIcon(com.intellij.icons.AllIcons.Nodes.Field)
.withTypeText("Hidden Field"));
}
}
}
}
// Collect any field/tag with NAME from LAYOUT
XmlTag layoutTag = entryTag.findFirstSubTag("LAYOUT");
if (layoutTag != null) {
addFieldsInLayoutRecursive(layoutTag, resultSet);
}
}
}
// 2. Search in included files
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);
addFormFieldsForNameRecursive(includedFile, resultSet, visited);
}
}
}
}
private void addFieldsInLayoutRecursive(XmlTag container, @NotNull CompletionResultSet resultSet) {
for (XmlTag subTag : container.getSubTags()) {
String name = subTag.getAttributeValue("NAME");
if (name != null && !name.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(name)
.withIcon(com.intellij.icons.AllIcons.Nodes.Field)
.withTypeText("Layout: " + subTag.getName()));
}
addFieldsInLayoutRecursive(subTag, resultSet);
}
}
}

View File

@@ -5,18 +5,203 @@ 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.PsiElement;
import com.intellij.psi.xml.XmlAttribute;
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.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.icons.AllIcons;
import java.awt.Color;
public class DynFormPathUtils {
public static final String MODULE_BASE_PATH = "src/main/webapp/WEB-INF/app/module";
public static 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(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(AllIcons.Nodes.Package)
.withItemTextForeground(Color.BLUE));
}
addFieldsInTagRecursive(subTag, resultSet);
}
}
}
public static void addFieldsInFormContext(PsiFile file, String containerName, @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;
// 1. Search for (containerName) > FIELDS in this file
List<XmlTag> containers = new ArrayList<>();
collectContainers(rootTag, containerName, containers);
for (XmlTag container : containers) {
XmlTag fieldsTag = container.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
addFieldsInTagRecursive(fieldsTag, resultSet);
}
}
// 2. Search in INCLUDES (Downward)
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
addFieldsInFormContext(includedFile, containerName, resultSet, visited);
}
}
}
// 3. Search in Includers (Upward)
if (visited.size() == 1) {
List<PsiFile> includers = findIncluders(file);
for (PsiFile includer : includers) {
addFieldsInFormContext(includer, containerName, resultSet, visited);
}
}
}
@Nullable
public 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()) || "SECTIONS".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;
}
public static void collectContainers(XmlTag parent, String containerName, List<XmlTag> results) {
if (containerName.equals(parent.getName())) {
results.add(parent);
} else {
for (XmlTag subTag : parent.getSubTags()) {
collectContainers(subTag, containerName, results);
}
}
}
@Nullable
public static PsiElement findFieldInFormContext(PsiFile file, String containerName, String fieldName, 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;
// 1. Search in all containers matching containerName anywhere in the file
List<XmlTag> containers = new ArrayList<>();
collectContainers(rootTag, containerName, containers);
for (XmlTag container : containers) {
XmlTag fieldsTag = container.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
PsiElement found = findFieldInTag(fieldsTag, fieldName);
if (found != null) return found;
}
}
// 2. Search in INCLUDES (Downward)
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
PsiElement found = findFieldInFormContext(includedFile, containerName, fieldName, visited);
if (found != null) return found;
}
}
}
// 3. Search in Includers (Upward)
if (visited.size() == 1) {
List<PsiFile> includers = findIncluders(file);
for (PsiFile includer : includers) {
PsiElement found = findFieldInFormContext(includer, containerName, fieldName, visited);
if (found != null) return found;
}
}
return null;
}
@Nullable
public static PsiElement findUsageInFormContext(PsiFile file, String containerName, String fieldName, 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;
// 1. Search in all containers matching containerName
List<XmlTag> containers = new ArrayList<>();
collectContainers(rootTag, containerName, containers);
for (XmlTag container : containers) {
XmlTag layoutTag = container.findFirstSubTag("LAYOUT");
if (layoutTag != null) {
PsiElement found = findFieldInTag(layoutTag, fieldName);
if (found != null) return found;
}
XmlTag titlesTag = container.findFirstSubTag("TITLES");
if (titlesTag != null) {
PsiElement found = findFieldInTag(titlesTag, fieldName);
if (found != null) return found;
}
}
// 2. Search in INCLUDES (Downward)
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
PsiElement found = findUsageInFormContext(includedFile, containerName, fieldName, visited);
if (found != null) return found;
}
}
}
// 3. Search in Includers (Upward)
if (visited.size() == 1) {
List<PsiFile> includers = findIncluders(file);
for (PsiFile includer : includers) {
PsiElement found = findUsageInFormContext(includer, containerName, fieldName, visited);
if (found != null) return found;
}
}
return null;
}
@Nullable
public static VirtualFile getModuleBaseDir(@NotNull Project project) {
VirtualFile baseDir = project.getBaseDir();
@@ -155,19 +340,6 @@ public class DynFormPathUtils {
return null;
}
@NotNull
public static List<PsiFile> getAllFrmlFiles(@NotNull PsiFile contextFile) {
List<PsiFile> files = new ArrayList<>();
VirtualFile moduleDir = findModuleDir(contextFile);
if (moduleDir != null) {
VirtualFile frmDir = moduleDir.findFileByRelativePath("view/frm");
if (frmDir != null) {
collectFrmlFilesRecursive(frmDir, contextFile.getProject(), files);
}
}
return files;
}
@NotNull
public static List<PsiFile> findIncluders(@NotNull PsiFile includedFile) {
List<PsiFile> includers = new ArrayList<>();
@@ -175,41 +347,380 @@ public class DynFormPathUtils {
VirtualFile includedVFile = includedFile.getVirtualFile();
if (includedVFile == null) return includers;
VirtualFile moduleDir = findModuleDir(includedFile);
if (moduleDir == null) return includers;
// Optimization: Use ReferencesSearch to find files that point to this file via <INCLUDE FILE="...">
// This leverages IntelliJ's indices and is MUCH faster than scanning all files manually.
com.intellij.psi.search.searches.ReferencesSearch.search(includedFile).forEach(ref -> {
PsiElement element = ref.getElement();
if (element != null) {
PsiFile file = element.getContainingFile();
if (file != null && !includers.contains(file)) {
includers.add(file);
}
}
});
List<PsiFile> allFiles = getAllFrmlFiles(includedFile);
for (PsiFile file : allFiles) {
if (file.equals(includedFile)) continue;
if (!(file instanceof XmlFile xmlFile)) continue;
// Fallback: If for some reason index-based search yields nothing,
// we might do a limited scan, but let's stick to indices for performance to avoid freezing.
return includers;
}
public static void addDatasetsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited, boolean includeAjax) {
if (file == null || !visited.add(file)) return;
if (includeAjax) {
// สำหรับ AJAX-OPTION ให้หาใน ajax.xml เท่านั้น
PsiFile ajaxFile = findAjaxXml(file);
if (ajaxFile != null) {
addDatasetsInFile(ajaxFile, resultSet);
}
return;
}
// 1. Add datasets from current file
addDatasetsInFile(file, resultSet);
// 2. Add datasets from files that INCLUDE this file (Parent/Main Files)
if (visited.size() == 1) {
List<PsiFile> includers = findIncluders(file);
for (PsiFile includer : includers) {
if (visited.add(includer)) {
addDatasetsInFile(includer, resultSet);
}
}
}
// 3. Add datasets from included files (Downward)
if (file instanceof XmlFile xmlFile) {
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return includers;
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile resolved = findIncludedFile(file, path);
if (resolved != null && resolved.getVirtualFile().equals(includedVFile)) {
includers.add(file);
if (rootTag != null) {
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
if (includedFile != null && !visited.contains(includedFile)) {
addDatasetsInFileRecursive(includedFile, resultSet, visited, includeAjax);
}
}
}
}
}
}
return includers;
}
private static void collectFrmlFilesRecursive(VirtualFile dir, Project project, List<PsiFile> files) {
for (VirtualFile child : dir.getChildren()) {
if (child.isDirectory()) {
collectFrmlFilesRecursive(child, project, files);
} else if (child.getName().endsWith(".frml")) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(child);
if (psiFile != null) files.add(psiFile);
public static 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(AllIcons.Nodes.DataTables)
.withTypeText(datasetTag.getAttributeValue("TABLENAME")));
}
}
}
public static void addGridsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited) {
if (file == null || !visited.add(file)) return;
// 1. Add grids from current file
addGridsInFile(file, resultSet);
// 2. Add grids from files that INCLUDE this file (Parent/Main Files)
if (visited.size() == 1) {
List<PsiFile> includers = findIncluders(file);
for (PsiFile includer : includers) {
if (visited.add(includer)) {
addGridsInFile(includer, resultSet);
}
}
}
// 3. Add grids from included files (Downward)
if (file instanceof XmlFile xmlFile) {
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag != null) {
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
if (includedFile != null && !visited.contains(includedFile)) {
addGridsInFileRecursive(includedFile, resultSet, visited);
}
}
}
}
}
}
}
public static void addGridsInFile(PsiFile file, @NotNull CompletionResultSet resultSet) {
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(AllIcons.Nodes.DataTables));
}
}
}
}
@Nullable
public static PsiElement findDatasetElement(PsiFile file, String id) {
Set<PsiFile> visited = new HashSet<>();
// 1. Local
PsiElement found = findDatasetInFile(file, id);
if (found != null) return found;
visited.add(file);
// 2. Includers
List<PsiFile> includers = findIncluders(file);
for (PsiFile includer : includers) {
found = findDatasetInFile(includer, id);
if (found != null) return found;
visited.add(includer);
}
// 3. Ajax
PsiFile ajaxXml = findAjaxXml(file);
if (ajaxXml != null && visited.add(ajaxXml)) {
found = findDatasetInFile(ajaxXml, id);
if (found != null) return found;
}
// 4. Recursive search
return findDatasetInFileRecursiveForCompletion(file, id, visited);
}
@Nullable
public static 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"))) {
return datasetTag;
}
}
return null;
}
@Nullable
public static PsiElement findDatasetInFileRecursiveForCompletion(PsiFile file, String id, Set<PsiFile> visited) {
if (file == null || !visited.add(file)) return null;
if (!(file instanceof XmlFile xmlFile)) return null;
// Try local
PsiElement found = findDatasetInFile(xmlFile, id);
if (found != null) return found;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null;
// Downward search
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
found = findDatasetInFileRecursiveForCompletion(includedFile, id, visited);
if (found != null) return found;
}
}
}
return null;
}
public static void addFormFieldsForNameRecursive(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;
// 1. Search in current file FORM_ENTRY tags
for (XmlTag formTag : rootTag.findSubTags("FORM")) {
for (XmlTag entryTag : formTag.findSubTags("FORM_ENTRY")) {
// Collect hidden fields from FIELDS
XmlTag fieldsTag = entryTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
for (XmlTag fieldTag : fieldsTag.findSubTags("FIELD")) {
if ("HIDDEN".equals(fieldTag.getAttributeValue("INPUTTYPE"))) {
String name = fieldTag.getAttributeValue("NAME");
if (name != null && !name.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Field)
.withTypeText("Hidden Field"));
}
}
}
}
// Collect any field/tag with NAME from LAYOUT
XmlTag layoutTag = entryTag.findFirstSubTag("LAYOUT");
if (layoutTag != null) {
addFieldsInLayoutRecursive(layoutTag, resultSet);
}
}
}
// 2. Search in included files
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
addFormFieldsForNameRecursive(includedFile, resultSet, visited);
}
}
}
}
public static void addFieldsInLayoutRecursive(XmlTag container, @NotNull CompletionResultSet resultSet) {
for (XmlTag subTag : container.getSubTags()) {
String name = subTag.getAttributeValue("NAME");
if (name != null && !name.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(name)
.withIcon(AllIcons.Nodes.Field)
.withTypeText("Layout: " + subTag.getName()));
}
addFieldsInLayoutRecursive(subTag, resultSet);
}
}
@Nullable
public static PsiElement findFormFieldByNameRecursive(PsiFile file, String name, 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;
// 1. Search in current file FORM_ENTRY tags
for (XmlTag formTag : rootTag.findSubTags("FORM")) {
for (XmlTag entryTag : formTag.findSubTags("FORM_ENTRY")) {
// Check hidden fields in FIELDS
XmlTag fieldsTag = entryTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
for (XmlTag fieldTag : fieldsTag.findSubTags("FIELD")) {
if ("HIDDEN".equals(fieldTag.getAttributeValue("INPUTTYPE")) && name.equals(fieldTag.getAttributeValue("NAME"))) {
XmlAttribute nameAttr = fieldTag.getAttribute("NAME");
return nameAttr != null ? nameAttr.getValueElement() : fieldTag;
}
}
}
// Check fields in LAYOUT
XmlTag layoutTag = entryTag.findFirstSubTag("LAYOUT");
if (layoutTag != null) {
PsiElement found = findFieldInTag(layoutTag, name);
if (found != null) return found;
}
}
}
// 2. Search in included files
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
PsiElement found = findFormFieldByNameRecursive(includedFile, name, visited);
if (found != null) return found;
}
}
}
return null;
}
@Nullable
public static PsiElement findGridInFile(PsiFile file, String id) {
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"))) {
return gridTag;
}
}
}
return null;
}
@Nullable
public static PsiElement findGridElement(PsiFile file, String id) {
Set<PsiFile> visited = new HashSet<>();
// 1. Local
PsiElement found = findGridInFile(file, id);
if (found != null) return found;
visited.add(file);
// 2. Includers
List<PsiFile> includers = findIncluders(file);
for (PsiFile includer : includers) {
found = findGridInFile(includer, id);
if (found != null) return found;
visited.add(includer);
}
// 3. Recursive
return findGridInIncludesRecursive(file, id, visited);
}
@Nullable
private static PsiElement findGridInIncludesRecursive(PsiFile file, String id, Set<PsiFile> visited) {
if (file == null || !visited.add(file) || !(file instanceof XmlFile xmlFile)) return null;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null;
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = findIncludedFile(file, path);
PsiElement found = findGridInFile(includedFile, id);
if (found != null) return found;
found = findGridInIncludesRecursive(includedFile, id, visited);
if (found != null) return found;
}
}
}
return null;
}
public 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;
}
}

View File

@@ -56,7 +56,7 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
// 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"))),
.withParent(XmlPatterns.xmlTag().withName("FIELD", "SECTION", "SECTIONS"))),
new PsiReferenceProvider() {
@NotNull
@Override
@@ -74,12 +74,12 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
}
// เคส 1: อยู่ใน LAYOUT หรือ TITLES -> ลิงก์ไปหา FIELDS (นิยาม)
if (hasAncestorWithName(tag, "LAYOUT") || hasAncestorWithName(tag, "TITLES")) {
if (DynFormPathUtils.hasAncestorWithName(tag, "LAYOUT") || DynFormPathUtils.hasAncestorWithName(tag, "TITLES")) {
return new PsiReference[]{new DynFormFieldDefinitionReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
// เคส 2: อยู่ใน FIELDS (นิยาม) -> ลิงก์กลับไปหา LAYOUT หรือ TITLES (การใช้งาน)
if (hasAncestorWithName(tag, "FIELDS")) {
if (DynFormPathUtils.hasAncestorWithName(tag, "FIELDS")) {
return new PsiReference[]{new DynFormFieldUsageReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
@@ -123,7 +123,7 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
String tagName = tag.getName();
// Only process DATASET tag if it's inside FOREIGN-DATASETS
if ("DATASET".equals(tagName) && !hasAncestorWithName(tag, "FOREIGN-DATASETS")) {
if ("DATASET".equals(tagName) && !DynFormPathUtils.hasAncestorWithName(tag, "FOREIGN-DATASETS")) {
return PsiReference.EMPTY_ARRAY;
}
@@ -263,15 +263,6 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
});
}
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
private PsiNewExpression getNewDynFormExpression(PsiLiteralExpression literal) {
PsiElement parent = literal.getParent();
@@ -328,15 +319,23 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
}
@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())) {
while (container != null &&
!"FORM_ENTRY".equals(container.getName()) &&
!"FORM_BROWSE".equals(container.getName()) &&
!"GRID-LIST".equals(container.getName()) &&
!"GRID-EDITOR".equals(container.getName()) &&
!"FILTERS".equals(container.getName())) {
container = container.getParentTag();
}
if (container == null) {
XmlFile file = (XmlFile) myElement.getContainingFile();
container = file.getRootTag();
// If not in a specific container, search in all known containers across context
PsiElement found = DynFormPathUtils.findFieldInFormContext(myElement.getContainingFile(), "FORM_ENTRY", fieldName, new HashSet<>());
if (found != null) return found;
return DynFormPathUtils.findFieldInFormContext(myElement.getContainingFile(), "FORM_BROWSE", fieldName, new HashSet<>());
}
if (container == null) return null;
return findFieldInTag(container, fieldName);
// Search in current container's FIELDS across context
return DynFormPathUtils.findFieldInFormContext(myElement.getContainingFile(), container.getName(), fieldName, new HashSet<>());
}
}
@@ -353,28 +352,19 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
XmlTag tag = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
if (tag == null) return null;
// หา container (FORM_ENTRY, FORM_BROWSE, GRID-LIST หรือ FILTERS)
// หา container (FORM_ENTRY, FORM_BROWSE, GRID-LIST, GRID-EDITOR หรือ 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()) &&
!"GRID-EDITOR".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);
return DynFormPathUtils.findFieldInFormContext(myElement.getContainingFile(), container.getName(), fieldName, new HashSet<>());
}
}
@@ -401,41 +391,10 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
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;
return DynFormPathUtils.findUsageInFormContext(myElement.getContainingFile(), containerTag.getName(), fieldName, new HashSet<>());
}
}
@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> implements PsiPolyVariantReference {
private final String datasetId;
private final boolean isAjaxOption;
@@ -473,31 +432,15 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
// 3. ค้นหาในไฟล์ที่ถูก INCLUDE (Downward)
findDatasetInIncludesRecursive(currentFile, datasetId, results, new HashSet<>());
if (!results.isEmpty()) return filterOpenFiles(results, myElement.getProject());
// 4. ค้นหาแบบ Module-wide (fallback สุดท้าย)
List<PsiFile> allFiles = DynFormPathUtils.getAllFrmlFiles(currentFile);
for (PsiFile file : allFiles) {
if (file.equals(currentFile) || includers.contains(file)) continue;
findDatasetInFile(file, datasetId, results);
}
return filterOpenFiles(results, myElement.getProject());
}
private void findDatasetInFile(PsiFile file, String id, List<ResolveResult> results) {
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")) {
if (id.equals(datasetTag.getAttributeValue("ID"))) {
XmlAttribute idAttr = datasetTag.getAttribute("ID");
results.add(new PsiElementResolveResult(idAttr != null ? idAttr.getValueElement() : datasetTag));
}
PsiElement found = DynFormPathUtils.findDatasetInFile(file, id);
if (found instanceof XmlTag datasetTag) {
XmlAttribute idAttr = datasetTag.getAttribute("ID");
results.add(new PsiElementResolveResult(idAttr != null ? idAttr.getValueElement() : datasetTag));
}
}
@@ -573,7 +516,7 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
if (parentDataset != null && "DATASET".equals(parentDataset.getName())) {
XmlTag fieldsTag = parentDataset.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
PsiElement found = findFieldInTag(fieldsTag, fieldName);
PsiElement found = DynFormPathUtils.findFieldInTag(fieldsTag, fieldName);
if (found != null) results.add(new PsiElementResolveResult(found));
}
}
@@ -605,7 +548,7 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
if (datasetTag != null) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
PsiElement found = findFieldInTag(fieldsTag, fieldName);
PsiElement found = DynFormPathUtils.findFieldInTag(fieldsTag, fieldName);
if (found != null) results.add(new PsiElementResolveResult(found));
}
}
@@ -642,18 +585,10 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
}
private void findDatasetInFile(PsiFile file, String id, List<ResolveResult> results) {
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")) {
if (id.equals(datasetTag.getAttributeValue("ID"))) {
XmlAttribute idAttr = datasetTag.getAttribute("ID");
results.add(new PsiElementResolveResult(idAttr != null ? idAttr.getValueElement() : datasetTag));
}
PsiElement found = DynFormPathUtils.findDatasetInFile(file, id);
if (found instanceof XmlTag datasetTag) {
XmlAttribute idAttr = datasetTag.getAttribute("ID");
results.add(new PsiElementResolveResult(idAttr != null ? idAttr.getValueElement() : datasetTag));
}
}
@@ -707,14 +642,6 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
// 3. ค้นหาในไฟล์ที่ถูก INCLUDE (Downward)
findGridInIncludesRecursive(currentFile, gridId, results, new HashSet<>());
if (!results.isEmpty()) return filterOpenFiles(results, myElement.getProject());
// 4. ค้นหาแบบ Module-wide (fallback)
List<PsiFile> allFiles = DynFormPathUtils.getAllFrmlFiles(currentFile);
for (PsiFile file : allFiles) {
if (file.equals(currentFile) || includers.contains(file)) continue;
findGridInFile(file, gridId, results);
}
return filterOpenFiles(results, myElement.getProject());
}
@@ -800,7 +727,7 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
if (datasetId.equals(datasetTag.getAttributeValue("ID"))) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
return findFieldInTag(fieldsTag, fieldName);
return DynFormPathUtils.findFieldInTag(fieldsTag, fieldName);
}
return datasetTag;
}
@@ -831,51 +758,7 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
@Nullable
@Override
public PsiElement resolve() {
return findFieldInFormEntriesRecursive(myElement.getContainingFile(), fieldName, new HashSet<>());
}
@Nullable
private PsiElement findFieldInFormEntriesRecursive(PsiFile file, String name, 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;
// 1. Search in current file FORM_ENTRY tags
for (XmlTag formTag : rootTag.findSubTags("FORM")) {
for (XmlTag entryTag : formTag.findSubTags("FORM_ENTRY")) {
// Check hidden fields in FIELDS
XmlTag fieldsTag = entryTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
for (XmlTag fieldTag : fieldsTag.findSubTags("FIELD")) {
if ("HIDDEN".equals(fieldTag.getAttributeValue("INPUTTYPE")) && name.equals(fieldTag.getAttributeValue("NAME"))) {
XmlAttribute nameAttr = fieldTag.getAttribute("NAME");
return nameAttr != null ? nameAttr.getValueElement() : fieldTag;
}
}
}
// Check fields in LAYOUT
XmlTag layoutTag = entryTag.findFirstSubTag("LAYOUT");
if (layoutTag != null) {
PsiElement found = findFieldInTag(layoutTag, name);
if (found != null) return found;
}
}
}
// 2. Search in included files
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 = findFieldInFormEntriesRecursive(includedFile, name, visited);
if (found != null) return found;
}
}
}
return null;
return DynFormPathUtils.findFormFieldByNameRecursive(myElement.getContainingFile(), fieldName, new HashSet<>());
}
}
@@ -951,33 +834,17 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
if (dataId != null && !dataId.isEmpty()) {
PsiFile file = gridTag.getContainingFile();
PsiElement datasetElement = findDatasetElement(file, dataId);
PsiElement datasetElement = DynFormPathUtils.findDatasetElement(file, dataId);
if (datasetElement instanceof XmlTag datasetTag) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
return findFieldInTag(fieldsTag, fieldName);
return DynFormPathUtils.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];