Update plugin version to 3.2.8 and enhance DynForm references

- Add cross-file Dataset and Field resolution for included .frml files
- Prioritize opened files in editor for reference navigation
- Restrict AJAX-OPTION DATASET auto-completion to suggest from ajax.xml
- Update change notes for 3.2.8 release
This commit is contained in:
2026-04-29 10:11:13 +07:00
parent 8f011e637b
commit 13ff47b7ae
5 changed files with 443 additions and 168 deletions

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.7" version = "3.2.8"
repositories { repositories {
mavenCentral() mavenCentral()

View File

@@ -113,7 +113,18 @@ public class DynFormCompletionContributor extends CompletionContributor {
protected void addCompletions(@NotNull CompletionParameters parameters, protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context, @NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) { @NotNull CompletionResultSet resultSet) {
addDatasetsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>()); PsiElement position = parameters.getPosition();
XmlTag parentTag = PsiTreeUtil.getParentOfType(position, XmlTag.class);
boolean isAjaxOption = parentTag != null && "AJAX-OPTION".equals(parentTag.getName());
if (isAjaxOption) {
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(parameters.getOriginalFile());
if (ajaxFile != null) {
addDatasetsInFile(ajaxFile, resultSet);
}
} else {
addDatasetsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>(), false);
}
} }
}); });
@@ -268,19 +279,32 @@ public class DynFormCompletionContributor extends CompletionContributor {
} }
} }
private void addDatasetsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited) { private void addDatasetsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited, boolean includeAjax) {
if (file == null || !visited.add(file)) return; 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 // 1. Add datasets from current file
addDatasetsInFile(file, resultSet); addDatasetsInFile(file, resultSet);
// 2. Add datasets from ajax.xml (only if not already added) // 2. Add datasets from files that INCLUDE this file (Parent/Main Files)
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file); if (visited.size() == 1) {
if (ajaxFile != null && ajaxFile != file) { List<PsiFile> includers = DynFormPathUtils.findIncluders(file);
addDatasetsInFile(ajaxFile, resultSet); for (PsiFile includer : includers) {
if (visited.add(includer)) {
addDatasetsInFile(includer, resultSet);
}
}
} }
// 3. Add datasets from included files // 3. Add datasets from included files (Downward)
if (file instanceof XmlFile xmlFile) { if (file instanceof XmlFile xmlFile) {
XmlTag rootTag = xmlFile.getRootTag(); XmlTag rootTag = xmlFile.getRootTag();
if (rootTag != null) { if (rootTag != null) {
@@ -290,12 +314,23 @@ public class DynFormCompletionContributor extends CompletionContributor {
String path = includeTag.getAttributeValue("FILE"); String path = includeTag.getAttributeValue("FILE");
if (path != null) { if (path != null) {
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path); PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
addDatasetsInFileRecursive(includedFile, resultSet, visited); 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) { private void addDatasetsInFile(PsiFile file, @NotNull CompletionResultSet resultSet) {
@@ -318,6 +353,50 @@ public class DynFormCompletionContributor extends CompletionContributor {
private void addGridsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited) { private void addGridsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited) {
if (file == null || !visited.add(file)) return; 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; if (!(file instanceof XmlFile xmlFile)) return;
XmlTag rootTag = xmlFile.getRootTag(); XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return; if (rootTag == null) return;
@@ -332,17 +411,6 @@ public class DynFormCompletionContributor extends CompletionContributor {
} }
} }
} }
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 void addFieldsFromDatasetId(XmlTag tag, @NotNull CompletionResultSet resultSet) { private void addFieldsFromDatasetId(XmlTag tag, @NotNull CompletionResultSet resultSet) {
@@ -401,13 +469,34 @@ public class DynFormCompletionContributor extends CompletionContributor {
} }
private PsiElement findDatasetElement(PsiFile file, String id) { private PsiElement findDatasetElement(PsiFile file, String id) {
return findDatasetInFileRecursiveForCompletion(file, id, new HashSet<>()); 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 findDatasetInFileRecursiveForCompletion(PsiFile file, String id, Set<PsiFile> visited) { private PsiElement findDatasetInFile(PsiFile file, String id) {
if (file == null || !visited.add(file)) return null;
if (!(file instanceof XmlFile xmlFile)) return null; if (!(file instanceof XmlFile xmlFile)) return null;
XmlTag rootTag = xmlFile.getRootTag(); XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null; if (rootTag == null) return null;
@@ -419,24 +508,43 @@ public class DynFormCompletionContributor extends CompletionContributor {
return datasetTag; return datasetTag;
} }
} }
return null;
}
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file); private PsiElement findDatasetInFileRecursiveForCompletion(PsiFile file, String id, Set<PsiFile> visited) {
if (ajaxFile != null && ajaxFile != file) { if (file == null || !visited.add(file)) return null;
PsiElement found = findDatasetInFileRecursiveForCompletion(ajaxFile, id, visited); if (!(file instanceof XmlFile xmlFile)) return null;
if (found != null) return found;
}
// 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"); XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) { if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) { for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE"); String path = includeTag.getAttributeValue("FILE");
if (path != null) { if (path != null) {
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path); PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
PsiElement found = findDatasetInFileRecursiveForCompletion(includedFile, id, visited); found = findDatasetInFileRecursiveForCompletion(includedFile, id, visited);
if (found != null) return found; 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; return null;
} }

View File

@@ -5,6 +5,8 @@ import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager; import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -140,4 +142,62 @@ public class DynFormPathUtils {
} }
return null; 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<>();
Project project = includedFile.getProject();
VirtualFile includedVFile = includedFile.getVirtualFile();
if (includedVFile == null) return includers;
VirtualFile moduleDir = findModuleDir(includedFile);
if (moduleDir == null) return includers;
List<PsiFile> allFiles = getAllFrmlFiles(includedFile);
for (PsiFile file : allFiles) {
if (file.equals(includedFile)) continue;
if (!(file instanceof XmlFile xmlFile)) continue;
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);
}
}
}
}
}
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);
}
}
}
} }

View File

@@ -1,5 +1,7 @@
package com.sdk.dynform.tools.dynform; package com.sdk.dynform.tools.dynform;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.XmlPatterns; import com.intellij.patterns.XmlPatterns;
@@ -13,7 +15,9 @@ import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
public class DynFormReferenceContributor extends PsiReferenceContributor { public class DynFormReferenceContributor extends PsiReferenceContributor {
@@ -93,7 +97,12 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
XmlAttributeValue attrValue = (XmlAttributeValue) element; XmlAttributeValue attrValue = (XmlAttributeValue) element;
String value = attrValue.getValue(); String value = attrValue.getValue();
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY; if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
return new PsiReference[]{new DynFormDatasetReference(attrValue, new TextRange(1, value.length() + 1), value)};
XmlAttribute attr = (XmlAttribute) attrValue.getParent();
XmlTag parentTag = attr != null ? (XmlTag) attr.getParent() : null;
boolean isAjaxOption = parentTag != null && "AJAX-OPTION".equals(parentTag.getName());
return new PsiReference[]{new DynFormDatasetReference(attrValue, new TextRange(1, value.length() + 1), value, isAjaxOption)};
} }
}); });
@@ -369,55 +378,59 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
return null; return null;
} }
private static class DynFormDatasetReference extends PsiReferenceBase<XmlAttributeValue> { private static class DynFormDatasetReference extends PsiReferenceBase<XmlAttributeValue> implements PsiPolyVariantReference {
private final String datasetId; private final String datasetId;
public DynFormDatasetReference(@NotNull XmlAttributeValue element, TextRange range, String datasetId) { private final boolean isAjaxOption;
public DynFormDatasetReference(@NotNull XmlAttributeValue element, TextRange range, String datasetId, boolean isAjaxOption) {
super(element, range, true); super(element, range, true);
this.datasetId = datasetId; this.datasetId = datasetId;
} this.isAjaxOption = isAjaxOption;
@Nullable @Override public PsiElement resolve() {
return findDatasetInFileRecursive(myElement.getContainingFile(), datasetId, new HashSet<>());
} }
@Nullable @Override
private PsiElement findDatasetInFileRecursive(PsiFile file, String id, Set<PsiFile> visited) { public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
if (file == null || !visited.add(file)) return null; List<ResolveResult> results = new ArrayList<>();
if (!(file instanceof XmlFile xmlFile)) return null; PsiFile currentFile = myElement.getContainingFile();
// 1. Search in current file
PsiElement found = findDatasetInFile(xmlFile, id);
if (found != null) return found;
// 2. Search in ajax.xml (only from main file) if (isAjaxOption) {
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file); // สำหรับ AJAX-OPTION ให้หาใน ajax.xml เท่านั้น
if (ajaxFile != null && ajaxFile != file) { PsiFile ajaxXml = DynFormPathUtils.findAjaxXml(currentFile);
found = findDatasetInFile(ajaxFile, id); if (ajaxXml != null) {
if (found != null) return found; findDatasetInFile(ajaxXml, datasetId, results);
}
// 3. Search in included files
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);
found = findDatasetInFileRecursive(includedFile, id, visited);
if (found != null) return found;
}
}
} }
return filterOpenFiles(results, myElement.getProject());
} }
return null;
// 1. ค้นหาในไฟล์ปัจจุบัน
findDatasetInFile(currentFile, datasetId, results);
if (!results.isEmpty()) return filterOpenFiles(results, myElement.getProject());
// 2. ค้นหาในไฟล์ที่ INCLUDE ไฟล์นี้ (Parent/Main Files)
List<PsiFile> includers = DynFormPathUtils.findIncluders(currentFile);
for (PsiFile includer : includers) {
findDatasetInFile(includer, datasetId, results);
}
if (!results.isEmpty()) return filterOpenFiles(results, myElement.getProject());
// 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());
} }
@Nullable private void findDatasetInFile(PsiFile file, String id, List<ResolveResult> results) {
private PsiElement findDatasetInFile(PsiFile file, String id) { if (!(file instanceof XmlFile xmlFile)) return;
if (!(file instanceof XmlFile xmlFile)) return null;
XmlTag rootTag = xmlFile.getRootTag(); XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null; if (rootTag == null) return;
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS"); XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
if (datasetsContainer == null) datasetsContainer = rootTag; if (datasetsContainer == null) datasetsContainer = rootTag;
@@ -425,14 +438,48 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) { for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
if (id.equals(datasetTag.getAttributeValue("ID"))) { if (id.equals(datasetTag.getAttributeValue("ID"))) {
XmlAttribute idAttr = datasetTag.getAttribute("ID"); XmlAttribute idAttr = datasetTag.getAttribute("ID");
return idAttr != null ? idAttr.getValueElement() : datasetTag; results.add(new PsiElementResolveResult(idAttr != null ? idAttr.getValueElement() : datasetTag));
} }
} }
return null; }
private void findDatasetInIncludesRecursive(PsiFile file, String id, List<ResolveResult> results, Set<PsiFile> visited) {
if (file == null || !visited.add(file) || !(file instanceof XmlFile xmlFile)) return;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return;
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) {
findDatasetInFile(includedFile, id, results);
if (results.isEmpty()) {
findDatasetInIncludesRecursive(includedFile, id, results, visited);
}
}
}
}
}
}
@Nullable
@Override
public PsiElement resolve() {
ResolveResult[] results = multiResolve(false);
return results.length == 1 ? results[0].getElement() : null;
}
@Override
public Object @NotNull [] getVariants() {
return new Object[0];
} }
} }
private static class DynFormMasterDataFieldReference extends PsiReferenceBase<XmlAttributeValue> { private static class DynFormMasterDataFieldReference extends PsiReferenceBase<XmlAttributeValue> implements PsiPolyVariantReference {
private final String fieldName; private final String fieldName;
public DynFormMasterDataFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) { public DynFormMasterDataFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
@@ -440,9 +487,9 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
this.fieldName = fieldName; this.fieldName = fieldName;
} }
@Nullable
@Override @Override
public PsiElement resolve() { public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
List<ResolveResult> results = new ArrayList<>();
XmlAttribute attr = (XmlAttribute) myElement.getParent(); XmlAttribute attr = (XmlAttribute) myElement.getParent();
XmlTag tag = (XmlTag) attr.getParent(); XmlTag tag = (XmlTag) attr.getParent();
String tagName = tag.getName(); String tagName = tag.getName();
@@ -450,73 +497,96 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
if ("MASTER-DATA".equals(tagName)) { if ("MASTER-DATA".equals(tagName)) {
if ("MASTER-FIELDS".equals(attrName)) { if ("MASTER-FIELDS".equals(attrName)) {
// MASTER-FIELDS refers to the DATASET specified in DATASET-ID resolveInDataset(tag.getAttributeValue("DATASET-ID"), results);
return resolveInDataset(tag.getAttributeValue("DATASET-ID"));
} else if ("DETAIL-FIELDS".equals(attrName)) { } else if ("DETAIL-FIELDS".equals(attrName)) {
// DETAIL-FIELDS refers to fields in current DATA-GRID
XmlTag gridTag = tag.getParentTag(); XmlTag gridTag = tag.getParentTag();
if (gridTag != null && "DATA-GRID".equals(gridTag.getName())) { if (gridTag != null && "DATA-GRID".equals(gridTag.getName())) {
return findFieldInGrid(gridTag); PsiElement found = findFieldInGrid(gridTag);
if (found != null) results.add(new PsiElementResolveResult(found));
} }
} }
} else if ("DATASET".equals(tagName)) { } else if ("DATASET".equals(tagName)) {
if ("DETAIL-FIELDS".equals(attrName)) { if ("DETAIL-FIELDS".equals(attrName)) {
// DETAIL-FIELDS refers to the DATASET specified in DATASET-ID resolveInDataset(tag.getAttributeValue("DATASET-ID"), results);
return resolveInDataset(tag.getAttributeValue("DATASET-ID"));
} else if ("MASTER-FIELDS".equals(attrName)) { } else if ("MASTER-FIELDS".equals(attrName)) {
// MASTER-FIELDS refers to parent DATASET
XmlTag foreignDatasetsTag = tag.getParentTag(); XmlTag foreignDatasetsTag = tag.getParentTag();
if (foreignDatasetsTag != null) { if (foreignDatasetsTag != null) {
XmlTag parentDataset = foreignDatasetsTag.getParentTag(); XmlTag parentDataset = foreignDatasetsTag.getParentTag();
if (parentDataset != null && "DATASET".equals(parentDataset.getName())) { if (parentDataset != null && "DATASET".equals(parentDataset.getName())) {
XmlTag fieldsTag = parentDataset.findFirstSubTag("FIELDS"); XmlTag fieldsTag = parentDataset.findFirstSubTag("FIELDS");
if (fieldsTag != null) { if (fieldsTag != null) {
return findFieldInTag(fieldsTag, fieldName); PsiElement found = findFieldInTag(fieldsTag, fieldName);
if (found != null) results.add(new PsiElementResolveResult(found));
} }
} }
} }
} }
} }
return null; return filterOpenFiles(results, myElement.getProject());
} }
@Nullable @Nullable
private PsiElement resolveInDataset(String datasetId) { @Override
if (datasetId == null || datasetId.isEmpty()) return null; public PsiElement resolve() {
ResolveResult[] results = multiResolve(false);
return results.length == 1 ? results[0].getElement() : null;
}
private void resolveInDataset(String datasetId, List<ResolveResult> results) {
if (datasetId == null || datasetId.isEmpty()) return;
// Re-use DynFormDatasetReference's finding logic if possible, or just look up List<PsiElement> datasetElements = findDatasetElements(datasetId);
PsiElement datasetElement = findDatasetElement(datasetId); for (PsiElement datasetElement : datasetElements) {
if (datasetElement instanceof XmlTag datasetTag) { XmlTag datasetTag = null;
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS"); if (datasetElement instanceof XmlTag) datasetTag = (XmlTag) datasetElement;
if (fieldsTag != null) { else if (datasetElement instanceof XmlAttributeValue) {
return findFieldInTag(fieldsTag, fieldName); XmlAttribute attr = (XmlAttribute) datasetElement.getParent();
datasetTag = attr.getParent();
}
if (datasetTag != null) {
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
PsiElement found = findFieldInTag(fieldsTag, fieldName);
if (found != null) results.add(new PsiElementResolveResult(found));
}
} }
} }
return null;
} }
@Nullable private List<PsiElement> findDatasetElements(String id) {
private PsiElement findDatasetElement(String id) { List<PsiElement> elements = new ArrayList<>();
// This is a simplified lookup, similar to DynFormDatasetReference's resolve PsiFile currentFile = myElement.getContainingFile();
PsiFile file = myElement.getContainingFile();
PsiElement found = findDatasetInFileRecursive(file, id, new HashSet<>()); // ใช้ Logic เดียวกับ DynFormDatasetReference เพื่อความถูกต้อง
if (found instanceof XmlAttributeValue attrValue) { List<ResolveResult> results = new ArrayList<>();
PsiElement parent = attrValue.getParent();
if (parent instanceof XmlAttribute attr) { // 1. Local
return attr.getParent(); findDatasetInFile(currentFile, id, results);
// 2. Includers
if (results.isEmpty()) {
for (PsiFile includer : DynFormPathUtils.findIncluders(currentFile)) {
findDatasetInFile(includer, id, results);
} }
} }
if (found instanceof XmlTag) return found;
return null; // 3. Ajax
if (results.isEmpty()) {
PsiFile ajaxXml = DynFormPathUtils.findAjaxXml(currentFile);
if (ajaxXml != null) findDatasetInFile(ajaxXml, id, results);
}
for (ResolveResult res : results) {
elements.add(res.getElement());
}
return elements;
} }
@Nullable private void findDatasetInFile(PsiFile file, String id, List<ResolveResult> results) {
private PsiElement findDatasetInFileRecursive(PsiFile file, String id, Set<PsiFile> visited) { if (!(file instanceof XmlFile xmlFile)) return;
if (file == null || !visited.add(file)) return null;
if (!(file instanceof XmlFile xmlFile)) return null;
XmlTag rootTag = xmlFile.getRootTag(); XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null; if (rootTag == null) return;
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS"); XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
if (datasetsContainer == null) datasetsContainer = rootTag; if (datasetsContainer == null) datasetsContainer = rootTag;
@@ -524,97 +594,94 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) { for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
if (id.equals(datasetTag.getAttributeValue("ID"))) { if (id.equals(datasetTag.getAttributeValue("ID"))) {
XmlAttribute idAttr = datasetTag.getAttribute("ID"); XmlAttribute idAttr = datasetTag.getAttribute("ID");
return idAttr != null ? idAttr.getValueElement() : datasetTag; results.add(new PsiElementResolveResult(idAttr != null ? idAttr.getValueElement() : datasetTag));
} }
} }
// Search in ajax.xml
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file);
if (ajaxFile != null && ajaxFile != file) {
PsiElement found = findDatasetInFileRecursive(ajaxFile, id, visited);
if (found != null) return found;
}
// Search in includes
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 = findDatasetInFileRecursive(includedFile, id, visited);
if (found != null) return found;
}
}
}
return null;
} }
@Nullable @Nullable
private PsiElement findFieldInGrid(XmlTag gridTag) { private PsiElement findFieldInGrid(XmlTag gridTag) {
// 1. Try to get DATAID from GRID-LIST or GRID-EDITOR
String dataId = null; String dataId = null;
XmlTag listTag = gridTag.findFirstSubTag("GRID-LIST"); XmlTag listTag = gridTag.findFirstSubTag("GRID-LIST");
if (listTag != null) { if (listTag != null) dataId = listTag.getAttributeValue("DATAID");
dataId = listTag.getAttributeValue("DATAID");
}
if (dataId == null) { if (dataId == null) {
XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR"); XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR");
if (editorTag != null) { if (editorTag != null) dataId = editorTag.getAttributeValue("DATAID");
dataId = editorTag.getAttributeValue("DATAID");
}
} }
// 2. Resolve fields from that DATASET
if (dataId != null && !dataId.isEmpty()) { if (dataId != null && !dataId.isEmpty()) {
return resolveInDataset(dataId); List<ResolveResult> results = new ArrayList<>();
} resolveInDataset(dataId, results);
return !results.isEmpty() ? results.get(0).getElement() : null;
// Fallback: Check for inline FIELDS in GRID-LIST/GRID-EDITOR (if any)
if (listTag != null) {
XmlTag fieldsTag = listTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
PsiElement found = findFieldInTag(fieldsTag, fieldName);
if (found != null) return found;
}
}
XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR");
if (editorTag != null) {
XmlTag fieldsTag = editorTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
return findFieldInTag(fieldsTag, fieldName);
}
} }
return null; return null;
} }
@Override
public Object @NotNull [] getVariants() {
return new Object[0];
}
} }
private static class DynFormGridReference extends PsiReferenceBase<XmlAttributeValue> { private static class DynFormGridReference extends PsiReferenceBase<XmlAttributeValue> implements PsiPolyVariantReference {
private final String gridId; private final String gridId;
public DynFormGridReference(@NotNull XmlAttributeValue element, TextRange range, String gridId) { public DynFormGridReference(@NotNull XmlAttributeValue element, TextRange range, String gridId) {
super(element, range, true); super(element, range, true);
this.gridId = gridId; this.gridId = gridId;
} }
@Nullable @Override public PsiElement resolve() {
return findGridInFileRecursive(myElement.getContainingFile(), gridId, new HashSet<>()); @Override
public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) {
List<ResolveResult> results = new ArrayList<>();
PsiFile currentFile = myElement.getContainingFile();
// 1. ค้นหาในไฟล์ปัจจุบัน
findGridInFile(currentFile, gridId, results);
if (!results.isEmpty()) return filterOpenFiles(results, myElement.getProject());
// 2. ค้นหาในไฟล์ที่ INCLUDE ไฟล์นี้ (Parent/Main Files)
List<PsiFile> includers = DynFormPathUtils.findIncluders(currentFile);
for (PsiFile includer : includers) {
findGridInFile(includer, gridId, results);
}
if (!results.isEmpty()) return filterOpenFiles(results, myElement.getProject());
// 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());
} }
@Nullable private void findGridInFile(PsiFile file, String id, List<ResolveResult> results) {
private PsiElement findGridInFileRecursive(PsiFile file, String id, Set<PsiFile> visited) { if (!(file instanceof XmlFile xmlFile)) return;
if (file == null || !visited.add(file)) return null;
if (!(file instanceof XmlFile xmlFile)) return null;
XmlTag rootTag = xmlFile.getRootTag(); XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null; if (rootTag == null) return;
XmlTag dataGridsTag = rootTag.findFirstSubTag("DATA-GRIDS"); XmlTag dataGridsTag = rootTag.findFirstSubTag("DATA-GRIDS");
if (dataGridsTag != null) { if (dataGridsTag != null) {
for (XmlTag gridTag : dataGridsTag.findSubTags("DATA-GRID")) { for (XmlTag gridTag : dataGridsTag.findSubTags("DATA-GRID")) {
if (id.equals(gridTag.getAttributeValue("ID"))) { if (id.equals(gridTag.getAttributeValue("ID"))) {
XmlAttribute idAttr = gridTag.getAttribute("ID"); XmlAttribute idAttr = gridTag.getAttribute("ID");
return idAttr != null ? idAttr.getValueElement() : gridTag; results.add(new PsiElementResolveResult(idAttr != null ? idAttr.getValueElement() : gridTag));
} }
} }
} }
}
private void findGridInIncludesRecursive(PsiFile file, String id, List<ResolveResult> results, Set<PsiFile> visited) {
if (file == null || !visited.add(file) || !(file instanceof XmlFile xmlFile)) return;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return;
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES"); XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) { if (includesTag != null) {
@@ -622,12 +689,27 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
String path = includeTag.getAttributeValue("FILE"); String path = includeTag.getAttributeValue("FILE");
if (path != null) { if (path != null) {
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path); PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
PsiElement found = findGridInFileRecursive(includedFile, id, visited); if (includedFile != null) {
if (found != null) return found; findGridInFile(includedFile, id, results);
if (results.isEmpty()) {
findGridInIncludesRecursive(includedFile, id, results, visited);
}
}
} }
} }
} }
return null; }
@Nullable
@Override
public PsiElement resolve() {
ResolveResult[] results = multiResolve(false);
return results.length == 1 ? results[0].getElement() : null;
}
@Override
public Object @NotNull [] getVariants() {
return new Object[0];
} }
} }
@@ -738,4 +820,23 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
return null; return null;
} }
} }
private static ResolveResult[] filterOpenFiles(List<ResolveResult> results, Project project) {
if (results.size() <= 1) return results.toArray(new ResolveResult[0]);
FileEditorManager editorManager = FileEditorManager.getInstance(project);
List<ResolveResult> openFiles = new ArrayList<>();
for (ResolveResult result : results) {
PsiElement element = result.getElement();
if (element != null) {
PsiFile file = element.getContainingFile();
if (file != null && file.getVirtualFile() != null && editorManager.isFileOpen(file.getVirtualFile())) {
openFiles.add(result);
}
}
}
if (!openFiles.isEmpty()) {
return openFiles.toArray(new ResolveResult[0]);
}
return results.toArray(new ResolveResult[0]);
}
} }

View File

@@ -34,6 +34,12 @@
]]></description> ]]></description>
<change-notes><![CDATA[ <change-notes><![CDATA[
<h2>[3.2.8]</h2>
<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>Opened File Priority:</strong> Reference navigation (Ctrl+Click) now prioritizes linking directly to files currently opened in editor tabs when multiple matches exist.</li>
<li><strong>Contextual AJAX-OPTION Completion:</strong> Code completion for `DATASET` under `<AJAX-OPTION>` is now strictly isolated to suggest entries defined exclusively in the module's `ajax.xml`.</li>
</ul>
<h2>[3.2.7]</h2> <h2>[3.2.7]</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>