diff --git a/build.gradle.kts b/build.gradle.kts index c32cc6e..cccc436 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("org.jetbrains.intellij.platform") version "2.7.0" } group = "com.sdk.dynform.tools" -version = "3.2.7" +version = "3.2.8" repositories { mavenCentral() diff --git a/src/main/java/com/sdk/dynform/tools/dynform/DynFormCompletionContributor.java b/src/main/java/com/sdk/dynform/tools/dynform/DynFormCompletionContributor.java index 98fa80a..3da8b43 100644 --- a/src/main/java/com/sdk/dynform/tools/dynform/DynFormCompletionContributor.java +++ b/src/main/java/com/sdk/dynform/tools/dynform/DynFormCompletionContributor.java @@ -113,7 +113,18 @@ public class DynFormCompletionContributor extends CompletionContributor { protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @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 visited) { + private void addDatasetsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set 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 ajax.xml (only if not already added) - PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file); - if (ajaxFile != null && ajaxFile != file) { - addDatasetsInFile(ajaxFile, resultSet); + // 2. Add datasets from files that INCLUDE this file (Parent/Main Files) + if (visited.size() == 1) { + List includers = DynFormPathUtils.findIncluders(file); + 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) { XmlTag rootTag = xmlFile.getRootTag(); if (rootTag != null) { @@ -290,12 +314,23 @@ public class DynFormCompletionContributor extends CompletionContributor { String path = includeTag.getAttributeValue("FILE"); if (path != null) { 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) { @@ -318,6 +353,50 @@ public class DynFormCompletionContributor extends CompletionContributor { private void addGridsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set 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 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; @@ -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) { @@ -401,13 +469,34 @@ public class DynFormCompletionContributor extends CompletionContributor { } private PsiElement findDatasetElement(PsiFile file, String id) { - return findDatasetInFileRecursiveForCompletion(file, id, new HashSet<>()); + Set visited = new HashSet<>(); + + // 1. Local + PsiElement found = findDatasetInFile(file, id); + if (found != null) return found; + visited.add(file); + + // 2. Includers + List 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 visited) { - if (file == null || !visited.add(file)) return null; + private PsiElement findDatasetInFile(PsiFile file, String id) { if (!(file instanceof XmlFile xmlFile)) return null; - XmlTag rootTag = xmlFile.getRootTag(); if (rootTag == null) return null; @@ -419,24 +508,43 @@ public class DynFormCompletionContributor extends CompletionContributor { return datasetTag; } } + return null; + } - PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file); - if (ajaxFile != null && ajaxFile != file) { - PsiElement found = findDatasetInFileRecursiveForCompletion(ajaxFile, id, visited); - if (found != null) return found; - } + private PsiElement findDatasetInFileRecursiveForCompletion(PsiFile file, String id, Set 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); - PsiElement found = findDatasetInFileRecursiveForCompletion(includedFile, id, visited); + 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; } diff --git a/src/main/java/com/sdk/dynform/tools/dynform/DynFormPathUtils.java b/src/main/java/com/sdk/dynform/tools/dynform/DynFormPathUtils.java index 5033a22..4d7fa91 100644 --- a/src/main/java/com/sdk/dynform/tools/dynform/DynFormPathUtils.java +++ b/src/main/java/com/sdk/dynform/tools/dynform/DynFormPathUtils.java @@ -5,6 +5,8 @@ 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.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -140,4 +142,62 @@ public class DynFormPathUtils { } return null; } + + @NotNull + public static List getAllFrmlFiles(@NotNull PsiFile contextFile) { + List 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 findIncluders(@NotNull PsiFile includedFile) { + List 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 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 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); + } + } + } } diff --git a/src/main/java/com/sdk/dynform/tools/dynform/DynFormReferenceContributor.java b/src/main/java/com/sdk/dynform/tools/dynform/DynFormReferenceContributor.java index 586bce1..e76d497 100644 --- a/src/main/java/com/sdk/dynform/tools/dynform/DynFormReferenceContributor.java +++ b/src/main/java/com/sdk/dynform/tools/dynform/DynFormReferenceContributor.java @@ -1,5 +1,7 @@ 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.patterns.PlatformPatterns; import com.intellij.patterns.XmlPatterns; @@ -13,7 +15,9 @@ import com.intellij.util.ProcessingContext; 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; public class DynFormReferenceContributor extends PsiReferenceContributor { @@ -93,7 +97,12 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { 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)}; + + 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; } - private static class DynFormDatasetReference extends PsiReferenceBase { + private static class DynFormDatasetReference extends PsiReferenceBase implements PsiPolyVariantReference { 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); this.datasetId = datasetId; - } - @Nullable @Override public PsiElement resolve() { - return findDatasetInFileRecursive(myElement.getContainingFile(), datasetId, new HashSet<>()); + this.isAjaxOption = isAjaxOption; } - @Nullable - private PsiElement findDatasetInFileRecursive(PsiFile file, String id, Set visited) { - if (file == null || !visited.add(file)) return null; - if (!(file instanceof XmlFile xmlFile)) return null; - - // 1. Search in current file - PsiElement found = findDatasetInFile(xmlFile, id); - if (found != null) return found; + @Override + public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { + List results = new ArrayList<>(); + PsiFile currentFile = myElement.getContainingFile(); - // 2. Search in ajax.xml (only from main file) - PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file); - if (ajaxFile != null && ajaxFile != file) { - found = findDatasetInFile(ajaxFile, id); - if (found != null) return found; - } - - // 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; - } - } + if (isAjaxOption) { + // สำหรับ AJAX-OPTION ให้หาใน ajax.xml เท่านั้น + PsiFile ajaxXml = DynFormPathUtils.findAjaxXml(currentFile); + if (ajaxXml != null) { + findDatasetInFile(ajaxXml, datasetId, results); } + 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 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 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 PsiElement findDatasetInFile(PsiFile file, String id) { - if (!(file instanceof XmlFile xmlFile)) return null; + private void findDatasetInFile(PsiFile file, String id, List results) { + if (!(file instanceof XmlFile xmlFile)) return; XmlTag rootTag = xmlFile.getRootTag(); - if (rootTag == null) return null; + if (rootTag == null) return; XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS"); if (datasetsContainer == null) datasetsContainer = rootTag; @@ -425,14 +438,48 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) { if (id.equals(datasetTag.getAttributeValue("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 results, Set 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 { + private static class DynFormMasterDataFieldReference extends PsiReferenceBase implements PsiPolyVariantReference { private final String fieldName; public DynFormMasterDataFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) { @@ -440,9 +487,9 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { this.fieldName = fieldName; } - @Nullable @Override - public PsiElement resolve() { + public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { + List results = new ArrayList<>(); XmlAttribute attr = (XmlAttribute) myElement.getParent(); XmlTag tag = (XmlTag) attr.getParent(); String tagName = tag.getName(); @@ -450,73 +497,96 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { if ("MASTER-DATA".equals(tagName)) { if ("MASTER-FIELDS".equals(attrName)) { - // MASTER-FIELDS refers to the DATASET specified in DATASET-ID - return resolveInDataset(tag.getAttributeValue("DATASET-ID")); + resolveInDataset(tag.getAttributeValue("DATASET-ID"), results); } else if ("DETAIL-FIELDS".equals(attrName)) { - // DETAIL-FIELDS refers to fields in current DATA-GRID XmlTag gridTag = tag.getParentTag(); 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)) { if ("DETAIL-FIELDS".equals(attrName)) { - // DETAIL-FIELDS refers to the DATASET specified in DATASET-ID - return resolveInDataset(tag.getAttributeValue("DATASET-ID")); + resolveInDataset(tag.getAttributeValue("DATASET-ID"), results); } else if ("MASTER-FIELDS".equals(attrName)) { - // MASTER-FIELDS refers to parent DATASET XmlTag foreignDatasetsTag = tag.getParentTag(); if (foreignDatasetsTag != null) { XmlTag parentDataset = foreignDatasetsTag.getParentTag(); if (parentDataset != null && "DATASET".equals(parentDataset.getName())) { XmlTag fieldsTag = parentDataset.findFirstSubTag("FIELDS"); 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 - private PsiElement resolveInDataset(String datasetId) { - if (datasetId == null || datasetId.isEmpty()) return null; + @Override + public PsiElement resolve() { + ResolveResult[] results = multiResolve(false); + return results.length == 1 ? results[0].getElement() : null; + } + + private void resolveInDataset(String datasetId, List results) { + if (datasetId == null || datasetId.isEmpty()) return; - // Re-use DynFormDatasetReference's finding logic if possible, or just look up - PsiElement datasetElement = findDatasetElement(datasetId); - if (datasetElement instanceof XmlTag datasetTag) { - XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS"); - if (fieldsTag != null) { - return findFieldInTag(fieldsTag, fieldName); + List datasetElements = findDatasetElements(datasetId); + for (PsiElement datasetElement : datasetElements) { + XmlTag datasetTag = null; + if (datasetElement instanceof XmlTag) datasetTag = (XmlTag) datasetElement; + else if (datasetElement instanceof XmlAttributeValue) { + 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 PsiElement findDatasetElement(String id) { - // This is a simplified lookup, similar to DynFormDatasetReference's resolve - PsiFile file = myElement.getContainingFile(); - PsiElement found = findDatasetInFileRecursive(file, id, new HashSet<>()); - if (found instanceof XmlAttributeValue attrValue) { - PsiElement parent = attrValue.getParent(); - if (parent instanceof XmlAttribute attr) { - return attr.getParent(); + private List findDatasetElements(String id) { + List elements = new ArrayList<>(); + PsiFile currentFile = myElement.getContainingFile(); + + // ใช้ Logic เดียวกับ DynFormDatasetReference เพื่อความถูกต้อง + List results = new ArrayList<>(); + + // 1. Local + 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 PsiElement findDatasetInFileRecursive(PsiFile file, String id, Set visited) { - if (file == null || !visited.add(file)) return null; - if (!(file instanceof XmlFile xmlFile)) return null; - + private void findDatasetInFile(PsiFile file, String id, List results) { + if (!(file instanceof XmlFile xmlFile)) return; XmlTag rootTag = xmlFile.getRootTag(); - if (rootTag == null) return null; + if (rootTag == null) return; XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS"); if (datasetsContainer == null) datasetsContainer = rootTag; @@ -524,97 +594,94 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) { if (id.equals(datasetTag.getAttributeValue("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 private PsiElement findFieldInGrid(XmlTag gridTag) { - // 1. Try to get DATAID from GRID-LIST or GRID-EDITOR String dataId = null; XmlTag listTag = gridTag.findFirstSubTag("GRID-LIST"); - if (listTag != null) { - dataId = listTag.getAttributeValue("DATAID"); - } + if (listTag != null) dataId = listTag.getAttributeValue("DATAID"); if (dataId == null) { XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR"); - if (editorTag != null) { - dataId = editorTag.getAttributeValue("DATAID"); - } + if (editorTag != null) dataId = editorTag.getAttributeValue("DATAID"); } - // 2. Resolve fields from that DATASET if (dataId != null && !dataId.isEmpty()) { - return resolveInDataset(dataId); - } - - // 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); - } + List results = new ArrayList<>(); + resolveInDataset(dataId, results); + return !results.isEmpty() ? results.get(0).getElement() : null; } return null; } + + @Override + public Object @NotNull [] getVariants() { + return new Object[0]; + } } - private static class DynFormGridReference extends PsiReferenceBase { + private static class DynFormGridReference extends PsiReferenceBase implements PsiPolyVariantReference { private final String gridId; + public DynFormGridReference(@NotNull XmlAttributeValue element, TextRange range, String gridId) { super(element, range, true); this.gridId = gridId; } - @Nullable @Override public PsiElement resolve() { - return findGridInFileRecursive(myElement.getContainingFile(), gridId, new HashSet<>()); + + @Override + public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { + List 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 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 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 PsiElement findGridInFileRecursive(PsiFile file, String id, Set visited) { - if (file == null || !visited.add(file)) return null; - if (!(file instanceof XmlFile xmlFile)) return null; + private void findGridInFile(PsiFile file, String id, List results) { + if (!(file instanceof XmlFile xmlFile)) return; XmlTag rootTag = xmlFile.getRootTag(); - if (rootTag == null) return null; + if (rootTag == null) return; 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; + results.add(new PsiElementResolveResult(idAttr != null ? idAttr.getValueElement() : gridTag)); } } } + } + + private void findGridInIncludesRecursive(PsiFile file, String id, List results, Set 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) { @@ -622,12 +689,27 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { 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; + if (includedFile != null) { + 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; } } + + private static ResolveResult[] filterOpenFiles(List results, Project project) { + if (results.size() <= 1) return results.toArray(new ResolveResult[0]); + FileEditorManager editorManager = FileEditorManager.getInstance(project); + List 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]); + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 404dd0b..e5372e8 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -34,6 +34,12 @@ ]]> [3.2.8] +
    +
  • Cross-file Reference Resolution: Enhanced resolution of Datasets and Fields (e.g., DS-MASTER) to search upward across included `.frml` files.
  • +
  • Opened File Priority: Reference navigation (Ctrl+Click) now prioritizes linking directly to files currently opened in editor tabs when multiple matches exist.
  • +
  • Contextual AJAX-OPTION Completion: Code completion for `DATASET` under `` is now strictly isolated to suggest entries defined exclusively in the module's `ajax.xml`.
  • +

[3.2.7]

  • Persistent Generation Context: Generator now remembers the last used directory for Action Beans and Dataset XMLs independently per project.