feat: implement advanced bidirectional field referencing and cross-module path resolution
- Core Logic Enhancements:
- Implement bidirectional field referencing between <FIELDS>, <LAYOUT>, and <TITLES> tags in .frml files, enabling seamless navigation from definitions to usages and vice versa.
- Add robust support for AJAX-OPTION field mapping:
- SRC attribute: Links to field definitions within defs/ajax.xml datasets.
- TARGET attribute: Links to local field definitions within the same form.
- Implement global grid resolution: GRID-ID now searches across the current file and all recursively included files (<INCLUDE>).
- Enhance deep recursive search for fields/sections within nested tags like <SECTION>, <ROW>, and <FIELD-LIST>.
- Path Resolution & Helpers (DynFormPathUtils):
- Added support for module-relative paths starting with # (mapping to view/frm/).
- Added support for cross-module paths starting with / (mapping to WEB-INF/app/module/{module}/view/frm/).
- Implemented auto-correction for common keyboard typos (Thai 'ิ' instead of /).
- Added specialized helpers for locating ajax.xml and included files within the framework's structure.
- Smart Completion Enhancements:
- Added context-aware completion for TARGET and SRC fields in AJAX update-fields.
- Enabled global GRID-ID completion by scanning all included resources.
- Improved dataset completion to include both local and AJAX-defined datasets.
- Test Resources:
- Added a comprehensive set of real-world examples (bdgt04, bdgt05, bdgt06) in DevResources/full-examples/ to validate complex cross-module and master-detail scenarios.
This commit is contained in:
@@ -3,15 +3,22 @@ package com.sdk.dynform.tools.helper;
|
||||
import com.intellij.codeInsight.completion.*;
|
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder;
|
||||
import com.intellij.patterns.PlatformPatterns;
|
||||
import com.intellij.patterns.XmlPatterns;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.psi.xml.XmlAttribute;
|
||||
import com.intellij.psi.xml.XmlFile;
|
||||
import com.intellij.psi.xml.XmlTag;
|
||||
import com.intellij.util.ProcessingContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class DynFormCompletionContributor extends CompletionContributor {
|
||||
public DynFormCompletionContributor() {
|
||||
// Java completion
|
||||
extend(CompletionType.BASIC, PlatformPatterns.psiElement().withParent(PsiLiteralExpression.class),
|
||||
new CompletionProvider<CompletionParameters>() {
|
||||
@Override
|
||||
@@ -27,7 +34,6 @@ public class DynFormCompletionContributor extends CompletionContributor {
|
||||
PsiExpressionList argList = newExp.getArgumentList();
|
||||
if (argList != null) {
|
||||
PsiExpression[] args = argList.getExpressions();
|
||||
// Suggest modules at index 3
|
||||
if (args.length >= 4 && args[3] == literal) {
|
||||
List<String> modules = DynFormPathUtils.getAllModules(position.getProject());
|
||||
for (String module : modules) {
|
||||
@@ -35,7 +41,6 @@ public class DynFormCompletionContributor extends CompletionContributor {
|
||||
.withIcon(com.intellij.icons.AllIcons.Nodes.Module));
|
||||
}
|
||||
}
|
||||
// Suggest frml files at index 4
|
||||
if (args.length >= 5 && args[4] == literal) {
|
||||
String moduleName = getArgumentValue(args, 3);
|
||||
if (moduleName != null) {
|
||||
@@ -50,6 +55,191 @@ public class DynFormCompletionContributor extends CompletionContributor {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// XML completion for Field/Section NAME/ID/TARGET
|
||||
extend(CompletionType.BASIC, XmlPatterns.psiElement()
|
||||
.inside(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("NAME", "ID", "TARGET")
|
||||
.withParent(XmlPatterns.xmlTag().withName("FIELD", "SECTION")))),
|
||||
new CompletionProvider<CompletionParameters>() {
|
||||
@Override
|
||||
protected void addCompletions(@NotNull CompletionParameters parameters,
|
||||
@NotNull ProcessingContext context,
|
||||
@NotNull CompletionResultSet resultSet) {
|
||||
PsiElement position = parameters.getPosition();
|
||||
XmlAttribute attr = PsiTreeUtil.getParentOfType(position, XmlAttribute.class);
|
||||
if (attr == null) return;
|
||||
|
||||
if ("TARGET".equals(attr.getName())) {
|
||||
// เสนอฟิลด์ในฟอร์มปัจจุบัน
|
||||
XmlTag formContainer = PsiTreeUtil.getParentOfType(position, XmlTag.class);
|
||||
while (formContainer != null && !"FORM_ENTRY".equals(formContainer.getName()) && !"FORM_BROWSE".equals(formContainer.getName())) {
|
||||
formContainer = formContainer.getParentTag();
|
||||
}
|
||||
|
||||
if (formContainer == null) {
|
||||
XmlFile file = (XmlFile) parameters.getOriginalFile();
|
||||
formContainer = file.getRootTag();
|
||||
}
|
||||
|
||||
if (formContainer != null) {
|
||||
addFieldsInTagRecursive(formContainer, resultSet);
|
||||
}
|
||||
} else {
|
||||
// เสนอฟิลด์ภายใต้ LAYOUT container เดียวกัน
|
||||
XmlTag layoutTag = PsiTreeUtil.getParentOfType(position, XmlTag.class);
|
||||
while (layoutTag != null && !"LAYOUT".equals(layoutTag.getName())) {
|
||||
layoutTag = layoutTag.getParentTag();
|
||||
}
|
||||
if (layoutTag == null) return;
|
||||
XmlTag containerTag = layoutTag.getParentTag();
|
||||
if (containerTag == null) return;
|
||||
XmlTag fieldsTag = containerTag.findFirstSubTag("FIELDS");
|
||||
if (fieldsTag != null) {
|
||||
addFieldsInTagRecursive(fieldsTag, resultSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// XML completion for Dataset ID
|
||||
extend(CompletionType.BASIC, XmlPatterns.psiElement()
|
||||
.inside(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("DATAID", "DATASET", "DATASET-ID"))),
|
||||
new CompletionProvider<CompletionParameters>() {
|
||||
@Override
|
||||
protected void addCompletions(@NotNull CompletionParameters parameters,
|
||||
@NotNull ProcessingContext context,
|
||||
@NotNull CompletionResultSet resultSet) {
|
||||
addDatasetsInFile(parameters.getOriginalFile(), resultSet);
|
||||
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(parameters.getOriginalFile());
|
||||
if (ajaxFile != null) {
|
||||
addDatasetsInFile(ajaxFile, resultSet);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// XML completion for SRC in UPDATE-FIELDS
|
||||
extend(CompletionType.BASIC, XmlPatterns.psiElement()
|
||||
.inside(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("SRC")
|
||||
.withParent(XmlPatterns.xmlTag().withName("FIELD")
|
||||
.withParent(XmlPatterns.xmlTag().withName("UPDATE-FIELDS"))))),
|
||||
new CompletionProvider<CompletionParameters>() {
|
||||
@Override
|
||||
protected void addCompletions(@NotNull CompletionParameters parameters,
|
||||
@NotNull ProcessingContext context,
|
||||
@NotNull CompletionResultSet resultSet) {
|
||||
PsiElement position = parameters.getPosition();
|
||||
XmlTag ajaxOptionTag = PsiTreeUtil.getParentOfType(position, XmlTag.class);
|
||||
while (ajaxOptionTag != null && !"AJAX-OPTION".equals(ajaxOptionTag.getName())) {
|
||||
ajaxOptionTag = ajaxOptionTag.getParentTag();
|
||||
}
|
||||
if (ajaxOptionTag == null) return;
|
||||
|
||||
String datasetId = ajaxOptionTag.getAttributeValue("DATASET");
|
||||
if (datasetId == null) return;
|
||||
|
||||
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(parameters.getOriginalFile());
|
||||
if (ajaxFile instanceof XmlFile xmlFile) {
|
||||
XmlTag rootTag = xmlFile.getRootTag();
|
||||
if (rootTag != null) {
|
||||
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
|
||||
if (datasetsContainer == null) datasetsContainer = rootTag;
|
||||
|
||||
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
|
||||
if (datasetId.equals(datasetTag.getAttributeValue("ID"))) {
|
||||
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
|
||||
if (fieldsTag != null) {
|
||||
addFieldsInTagRecursive(fieldsTag, resultSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// XML completion for GRID-ID
|
||||
extend(CompletionType.BASIC, XmlPatterns.psiElement()
|
||||
.inside(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("GRID-ID"))),
|
||||
new CompletionProvider<CompletionParameters>() {
|
||||
@Override
|
||||
protected void addCompletions(@NotNull CompletionParameters parameters,
|
||||
@NotNull ProcessingContext context,
|
||||
@NotNull CompletionResultSet resultSet) {
|
||||
addGridsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addFieldsInTagRecursive(XmlTag container, @NotNull CompletionResultSet resultSet) {
|
||||
for (XmlTag subTag : container.getSubTags()) {
|
||||
if ("FIELD".equals(subTag.getName())) {
|
||||
String name = subTag.getAttributeValue("NAME");
|
||||
if (name != null && !name.isEmpty()) {
|
||||
resultSet.addElement(LookupElementBuilder.create(name)
|
||||
.withIcon(com.intellij.icons.AllIcons.Nodes.Field)
|
||||
.withTypeText(subTag.getAttributeValue("TYPE")));
|
||||
}
|
||||
} else {
|
||||
if (("SECTION".equals(subTag.getName()) || "FIELDS".equals(subTag.getName())) && subTag.getAttributeValue("ID") != null) {
|
||||
String id = subTag.getAttributeValue("ID");
|
||||
resultSet.addElement(LookupElementBuilder.create(id)
|
||||
.withIcon(com.intellij.icons.AllIcons.Nodes.Package)
|
||||
.withItemTextForeground(java.awt.Color.BLUE));
|
||||
}
|
||||
addFieldsInTagRecursive(subTag, resultSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addDatasetsInFile(PsiFile file, @NotNull CompletionResultSet resultSet) {
|
||||
if (!(file instanceof XmlFile xmlFile)) return;
|
||||
XmlTag rootTag = xmlFile.getRootTag();
|
||||
if (rootTag == null) return;
|
||||
|
||||
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
|
||||
if (datasetsContainer == null) datasetsContainer = rootTag;
|
||||
|
||||
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
|
||||
String id = datasetTag.getAttributeValue("ID");
|
||||
if (id != null && !id.isEmpty()) {
|
||||
resultSet.addElement(LookupElementBuilder.create(id)
|
||||
.withIcon(com.intellij.icons.AllIcons.Nodes.DataTables)
|
||||
.withTypeText(datasetTag.getAttributeValue("TABLENAME")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addGridsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited) {
|
||||
if (file == null || !visited.add(file)) return;
|
||||
if (!(file instanceof XmlFile xmlFile)) return;
|
||||
XmlTag rootTag = xmlFile.getRootTag();
|
||||
if (rootTag == null) return;
|
||||
|
||||
XmlTag dataGridsTag = rootTag.findFirstSubTag("DATA-GRIDS");
|
||||
if (dataGridsTag != null) {
|
||||
for (XmlTag gridTag : dataGridsTag.findSubTags("DATA-GRID")) {
|
||||
String id = gridTag.getAttributeValue("ID");
|
||||
if (id != null && !id.isEmpty()) {
|
||||
resultSet.addElement(LookupElementBuilder.create(id)
|
||||
.withIcon(com.intellij.icons.AllIcons.Nodes.DataTables));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
|
||||
if (includesTag != null) {
|
||||
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
|
||||
String path = includeTag.getAttributeValue("FILE");
|
||||
if (path != null) {
|
||||
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
|
||||
addGridsInFileRecursive(includedFile, resultSet, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PsiNewExpression getNewDynFormExpression(PsiLiteralExpression literal) {
|
||||
|
||||
@@ -5,13 +5,10 @@ import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiDirectory;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.PsiManager;
|
||||
import com.intellij.psi.search.FilenameIndex;
|
||||
import com.intellij.psi.search.GlobalSearchScope;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class DynFormPathUtils {
|
||||
@@ -20,12 +17,75 @@ public class DynFormPathUtils {
|
||||
|
||||
@Nullable
|
||||
public static VirtualFile getModuleBaseDir(@NotNull Project project) {
|
||||
// ค้นหาจาก project root โดยตรง
|
||||
VirtualFile baseDir = project.getBaseDir();
|
||||
if (baseDir == null) return null;
|
||||
return baseDir.findFileByRelativePath(MODULE_BASE_PATH);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static VirtualFile findModuleDir(@NotNull PsiFile file) {
|
||||
VirtualFile vFile = file.getVirtualFile();
|
||||
if (vFile == null) return null;
|
||||
|
||||
VirtualFile current = vFile.getParent();
|
||||
while (current != null) {
|
||||
if (current.getParent() != null && "module".equals(current.getParent().getName())) {
|
||||
return current;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PsiFile findAjaxXml(@NotNull PsiFile file) {
|
||||
VirtualFile moduleDir = findModuleDir(file);
|
||||
if (moduleDir != null) {
|
||||
VirtualFile ajaxFile = moduleDir.findFileByRelativePath("defs/ajax.xml");
|
||||
if (ajaxFile != null) {
|
||||
return PsiManager.getInstance(file.getProject()).findFile(ajaxFile);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PsiFile findIncludedFile(@NotNull PsiFile contextFile, @NotNull String path) {
|
||||
// จัดการกรณี typo (ิ แทน /)
|
||||
path = path.replace("ิ", "/");
|
||||
|
||||
VirtualFile moduleBase = getModuleBaseDir(contextFile.getProject());
|
||||
if (moduleBase == null) return null;
|
||||
|
||||
VirtualFile targetVFile = null;
|
||||
if (path.startsWith("#")) {
|
||||
// ภายในโมดูลเดียวกัน: #grids/file.frml -> currentModule/view/frm/grids/file.frml
|
||||
VirtualFile moduleDir = findModuleDir(contextFile);
|
||||
if (moduleDir != null) {
|
||||
String relativePath = "view/frm/" + path.substring(1);
|
||||
targetVFile = moduleDir.findFileByRelativePath(relativePath);
|
||||
}
|
||||
} else if (path.startsWith("/")) {
|
||||
// ข้ามโมดูล: /bdgt04/grids/file.frml -> module/bdgt04/view/frm/grids/file.frml
|
||||
String cleanPath = path.substring(1);
|
||||
int firstSlash = cleanPath.indexOf("/");
|
||||
if (firstSlash > 0) {
|
||||
String targetModule = cleanPath.substring(0, firstSlash);
|
||||
String subPath = cleanPath.substring(firstSlash + 1);
|
||||
String fullPath = targetModule + "/view/frm/" + subPath;
|
||||
targetVFile = moduleBase.findFileByRelativePath(fullPath);
|
||||
}
|
||||
} else {
|
||||
// สัมพัทธ์กับไฟล์ปัจจุบัน
|
||||
targetVFile = contextFile.getVirtualFile().getParent().findFileByRelativePath(path);
|
||||
}
|
||||
|
||||
if (targetVFile != null) {
|
||||
return PsiManager.getInstance(contextFile.getProject()).findFile(targetVFile);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> getAllModules(@NotNull Project project) {
|
||||
List<String> modules = new ArrayList<>();
|
||||
|
||||
@@ -2,14 +2,24 @@ package com.sdk.dynform.tools.helper;
|
||||
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.patterns.PlatformPatterns;
|
||||
import com.intellij.patterns.XmlPatterns;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.psi.xml.XmlAttribute;
|
||||
import com.intellij.psi.xml.XmlAttributeValue;
|
||||
import com.intellij.psi.xml.XmlFile;
|
||||
import com.intellij.psi.xml.XmlTag;
|
||||
import com.intellij.util.ProcessingContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class DynFormReferenceContributor extends PsiReferenceContributor {
|
||||
@Override
|
||||
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
|
||||
// Java Reference Provider
|
||||
registrar.registerReferenceProvider(PlatformPatterns.psiElement(PsiLiteralExpression.class),
|
||||
new PsiReferenceProvider() {
|
||||
@NotNull
|
||||
@@ -24,11 +34,9 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
|
||||
PsiExpressionList argList = newExp.getArgumentList();
|
||||
if (argList != null) {
|
||||
PsiExpression[] args = argList.getExpressions();
|
||||
// Argument index 3 is moduleName
|
||||
if (args.length >= 4 && args[3] == literal) {
|
||||
return new PsiReference[]{new DynFormModuleReference(element, new TextRange(1, value.length() + 1), value)};
|
||||
}
|
||||
// Argument index 4 is frmlName
|
||||
if (args.length >= 5 && args[4] == literal) {
|
||||
String moduleName = getArgumentValue(args, 3);
|
||||
if (moduleName != null) {
|
||||
@@ -40,6 +48,109 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
|
||||
return PsiReference.EMPTY_ARRAY;
|
||||
}
|
||||
});
|
||||
|
||||
// XML Reference Provider for Field/Section NAME/ID
|
||||
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("NAME", "ID", "TARGET")
|
||||
.withParent(XmlPatterns.xmlTag().withName("FIELD", "SECTION"))),
|
||||
new PsiReferenceProvider() {
|
||||
@NotNull
|
||||
@Override
|
||||
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
|
||||
XmlAttributeValue attrValue = (XmlAttributeValue) element;
|
||||
String value = attrValue.getValue();
|
||||
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
|
||||
|
||||
XmlAttribute attr = (XmlAttribute) attrValue.getParent();
|
||||
XmlTag tag = (XmlTag) attr.getParent();
|
||||
String attrName = attr.getName();
|
||||
|
||||
if ("TARGET".equals(attrName)) {
|
||||
return new PsiReference[]{new DynFormLocalFieldReference(attrValue, new TextRange(1, value.length() + 1), value)};
|
||||
}
|
||||
|
||||
// เคส 1: อยู่ใน LAYOUT หรือ TITLES -> ลิงก์ไปหา FIELDS (นิยาม)
|
||||
if (hasAncestorWithName(tag, "LAYOUT") || hasAncestorWithName(tag, "TITLES")) {
|
||||
return new PsiReference[]{new DynFormFieldDefinitionReference(attrValue, new TextRange(1, value.length() + 1), value)};
|
||||
}
|
||||
|
||||
// เคส 2: อยู่ใน FIELDS (นิยาม) -> ลิงก์กลับไปหา LAYOUT หรือ TITLES (การใช้งาน)
|
||||
if (hasAncestorWithName(tag, "FIELDS")) {
|
||||
return new PsiReference[]{new DynFormFieldUsageReference(attrValue, new TextRange(1, value.length() + 1), value)};
|
||||
}
|
||||
|
||||
return PsiReference.EMPTY_ARRAY;
|
||||
}
|
||||
});
|
||||
|
||||
// XML Reference for Dataset ID (DATAID, DATASET, DATASET-ID)
|
||||
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("DATAID", "DATASET", "DATASET-ID")),
|
||||
new PsiReferenceProvider() {
|
||||
@NotNull
|
||||
@Override
|
||||
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
|
||||
XmlAttributeValue attrValue = (XmlAttributeValue) element;
|
||||
String value = attrValue.getValue();
|
||||
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
|
||||
return new PsiReference[]{new DynFormDatasetReference(attrValue, new TextRange(1, value.length() + 1), value)};
|
||||
}
|
||||
});
|
||||
|
||||
// XML Reference for Grid ID
|
||||
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("GRID-ID")),
|
||||
new PsiReferenceProvider() {
|
||||
@NotNull
|
||||
@Override
|
||||
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
|
||||
XmlAttributeValue attrValue = (XmlAttributeValue) element;
|
||||
String value = attrValue.getValue();
|
||||
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
|
||||
return new PsiReference[]{new DynFormGridReference(attrValue, new TextRange(1, value.length() + 1), value)};
|
||||
}
|
||||
});
|
||||
|
||||
// XML Reference for SRC in UPDATE-FIELDS
|
||||
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("SRC")
|
||||
.withParent(XmlPatterns.xmlTag().withName("FIELD")
|
||||
.withParent(XmlPatterns.xmlTag().withName("UPDATE-FIELDS")))),
|
||||
new PsiReferenceProvider() {
|
||||
@NotNull
|
||||
@Override
|
||||
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
|
||||
XmlAttributeValue attrValue = (XmlAttributeValue) element;
|
||||
String value = attrValue.getValue();
|
||||
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
|
||||
return new PsiReference[]{new DynFormAjaxSrcFieldReference(attrValue, new TextRange(1, value.length() + 1), value)};
|
||||
}
|
||||
});
|
||||
|
||||
// XML Reference for Include File
|
||||
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
|
||||
.withParent(XmlPatterns.xmlAttribute().withName("FILE")
|
||||
.withParent(XmlPatterns.xmlTag().withName("INCLUDE")
|
||||
.withParent(XmlPatterns.xmlTag().withName("INCLUDES")))),
|
||||
new PsiReferenceProvider() {
|
||||
@NotNull
|
||||
@Override
|
||||
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
|
||||
XmlAttributeValue attrValue = (XmlAttributeValue) element;
|
||||
String value = attrValue.getValue();
|
||||
if (value == null || value.isEmpty()) return PsiReference.EMPTY_ARRAY;
|
||||
return new PsiReference[]{new DynFormFileIncludeReference(attrValue, new TextRange(1, value.length() + 1), value)};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean hasAncestorWithName(XmlTag tag, String name) {
|
||||
XmlTag current = tag.getParentTag();
|
||||
while (current != null) {
|
||||
if (name.equals(current.getName())) return true;
|
||||
current = current.getParentTag();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -68,15 +179,11 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
|
||||
|
||||
private static class DynFormModuleReference extends PsiReferenceBase<PsiElement> {
|
||||
private final String moduleName;
|
||||
|
||||
public DynFormModuleReference(@NotNull PsiElement element, TextRange textRange, String moduleName) {
|
||||
super(element, textRange);
|
||||
this.moduleName = moduleName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public PsiElement resolve() {
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
return DynFormPathUtils.findModuleDirectory(myElement.getProject(), moduleName);
|
||||
}
|
||||
}
|
||||
@@ -84,17 +191,256 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
|
||||
private static class DynFormFrmlReference extends PsiReferenceBase<PsiElement> {
|
||||
private final String moduleName;
|
||||
private final String frmlName;
|
||||
|
||||
public DynFormFrmlReference(@NotNull PsiElement element, TextRange textRange, String moduleName, String frmlName) {
|
||||
super(element, textRange);
|
||||
this.moduleName = moduleName;
|
||||
this.frmlName = frmlName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public PsiElement resolve() {
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
return DynFormPathUtils.findFrmlFile(myElement.getProject(), moduleName, frmlName);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DynFormLocalFieldReference extends PsiReferenceBase<XmlAttributeValue> {
|
||||
private final String fieldName;
|
||||
public DynFormLocalFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
|
||||
super(element, range);
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
XmlTag container = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
|
||||
while (container != null && !"FORM_ENTRY".equals(container.getName()) && !"FORM_BROWSE".equals(container.getName()) && !"GRID-LIST".equals(container.getName())) {
|
||||
container = container.getParentTag();
|
||||
}
|
||||
if (container == null) {
|
||||
XmlFile file = (XmlFile) myElement.getContainingFile();
|
||||
container = file.getRootTag();
|
||||
}
|
||||
if (container == null) return null;
|
||||
return findFieldInTag(container, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ลิงก์จาก Layout หรือ Titles ไปหา FIELDS
|
||||
*/
|
||||
private static class DynFormFieldDefinitionReference extends PsiReferenceBase<XmlAttributeValue> {
|
||||
private final String fieldName;
|
||||
public DynFormFieldDefinitionReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
|
||||
super(element, range);
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
XmlTag tag = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
|
||||
if (tag == null) return null;
|
||||
|
||||
// หา container (FORM_ENTRY, FORM_BROWSE, GRID-LIST หรือ FILTERS)
|
||||
XmlTag container = tag;
|
||||
while (container != null &&
|
||||
!"LAYOUT".equals(container.getName()) &&
|
||||
!"TITLES".equals(container.getName()) &&
|
||||
!"FORM_ENTRY".equals(container.getName()) &&
|
||||
!"FORM_BROWSE".equals(container.getName()) &&
|
||||
!"GRID-LIST".equals(container.getName()) &&
|
||||
!"FILTERS".equals(container.getName())) {
|
||||
container = container.getParentTag();
|
||||
}
|
||||
|
||||
if (container != null && ("LAYOUT".equals(container.getName()) || "TITLES".equals(container.getName()))) {
|
||||
container = container.getParentTag();
|
||||
}
|
||||
|
||||
if (container == null) return null;
|
||||
|
||||
XmlTag fieldsTag = container.findFirstSubTag("FIELDS");
|
||||
if (fieldsTag == null) return null;
|
||||
|
||||
return findFieldInTag(fieldsTag, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ค้นหาย้อนกลับจากนิยามฟิลด์ (FIELDS) ไปยังการใช้งานใน LAYOUT หรือ TITLES
|
||||
*/
|
||||
private static class DynFormFieldUsageReference extends PsiReferenceBase<XmlAttributeValue> {
|
||||
private final String fieldName;
|
||||
public DynFormFieldUsageReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
|
||||
super(element, range);
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
XmlTag tag = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
|
||||
if (tag == null) return null;
|
||||
|
||||
// หา FIELDS container
|
||||
XmlTag fieldsTag = tag;
|
||||
while (fieldsTag != null && !"FIELDS".equals(fieldsTag.getName())) {
|
||||
fieldsTag = fieldsTag.getParentTag();
|
||||
}
|
||||
if (fieldsTag == null) return null;
|
||||
|
||||
XmlTag containerTag = fieldsTag.getParentTag();
|
||||
if (containerTag == null) return null;
|
||||
|
||||
// หาใน LAYOUT ก่อน
|
||||
XmlTag layoutTag = containerTag.findFirstSubTag("LAYOUT");
|
||||
if (layoutTag != null) {
|
||||
PsiElement found = findFieldInTag(layoutTag, fieldName);
|
||||
if (found != null) return found;
|
||||
}
|
||||
|
||||
// ถ้าไม่เจอใน LAYOUT ให้หาใน TITLES
|
||||
XmlTag titlesTag = containerTag.findFirstSubTag("TITLES");
|
||||
if (titlesTag != null) {
|
||||
PsiElement found = findFieldInTag(titlesTag, fieldName);
|
||||
if (found != null) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static PsiElement findFieldInTag(XmlTag container, String name) {
|
||||
for (XmlTag subTag : container.getSubTags()) {
|
||||
if ("FIELD".equals(subTag.getName()) && name.equals(subTag.getAttributeValue("NAME"))) {
|
||||
XmlAttribute nameAttr = subTag.getAttribute("NAME");
|
||||
return nameAttr != null ? nameAttr.getValueElement() : subTag;
|
||||
}
|
||||
if (("SECTION".equals(subTag.getName()) || "ROW".equals(subTag.getName())) && name.equals(subTag.getAttributeValue("ID"))) {
|
||||
XmlAttribute idAttr = subTag.getAttribute("ID");
|
||||
return idAttr != null ? idAttr.getValueElement() : subTag;
|
||||
}
|
||||
PsiElement found = findFieldInTag(subTag, name);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class DynFormDatasetReference extends PsiReferenceBase<XmlAttributeValue> {
|
||||
private final String datasetId;
|
||||
public DynFormDatasetReference(@NotNull XmlAttributeValue element, TextRange range, String datasetId) {
|
||||
super(element, range);
|
||||
this.datasetId = datasetId;
|
||||
}
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
PsiElement found = findDatasetInFile(myElement.getContainingFile(), datasetId);
|
||||
if (found != null) return found;
|
||||
|
||||
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(myElement.getContainingFile());
|
||||
if (ajaxFile != null) {
|
||||
return findDatasetInFile(ajaxFile, datasetId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PsiElement findDatasetInFile(PsiFile file, String id) {
|
||||
if (!(file instanceof XmlFile xmlFile)) return null;
|
||||
XmlTag rootTag = xmlFile.getRootTag();
|
||||
if (rootTag == null) return null;
|
||||
|
||||
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
|
||||
if (datasetsContainer == null) datasetsContainer = rootTag;
|
||||
|
||||
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
|
||||
if (id.equals(datasetTag.getAttributeValue("ID"))) {
|
||||
XmlAttribute idAttr = datasetTag.getAttribute("ID");
|
||||
return idAttr != null ? idAttr.getValueElement() : datasetTag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DynFormGridReference extends PsiReferenceBase<XmlAttributeValue> {
|
||||
private final String gridId;
|
||||
public DynFormGridReference(@NotNull XmlAttributeValue element, TextRange range, String gridId) {
|
||||
super(element, range);
|
||||
this.gridId = gridId;
|
||||
}
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
return findGridInFileRecursive(myElement.getContainingFile(), gridId, new HashSet<>());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PsiElement findGridInFileRecursive(PsiFile file, String id, Set<PsiFile> visited) {
|
||||
if (file == null || !visited.add(file)) return null;
|
||||
if (!(file instanceof XmlFile xmlFile)) return null;
|
||||
XmlTag rootTag = xmlFile.getRootTag();
|
||||
if (rootTag == null) return null;
|
||||
|
||||
XmlTag dataGridsTag = rootTag.findFirstSubTag("DATA-GRIDS");
|
||||
if (dataGridsTag != null) {
|
||||
for (XmlTag gridTag : dataGridsTag.findSubTags("DATA-GRID")) {
|
||||
if (id.equals(gridTag.getAttributeValue("ID"))) {
|
||||
XmlAttribute idAttr = gridTag.getAttribute("ID");
|
||||
return idAttr != null ? idAttr.getValueElement() : gridTag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
|
||||
if (includesTag != null) {
|
||||
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
|
||||
String path = includeTag.getAttributeValue("FILE");
|
||||
if (path != null) {
|
||||
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
|
||||
PsiElement found = findGridInFileRecursive(includedFile, id, visited);
|
||||
if (found != null) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DynFormAjaxSrcFieldReference extends PsiReferenceBase<XmlAttributeValue> {
|
||||
private final String fieldName;
|
||||
public DynFormAjaxSrcFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
|
||||
super(element, range);
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
XmlTag ajaxOptionTag = PsiTreeUtil.getParentOfType(myElement, XmlTag.class);
|
||||
while (ajaxOptionTag != null && !"AJAX-OPTION".equals(ajaxOptionTag.getName())) {
|
||||
ajaxOptionTag = ajaxOptionTag.getParentTag();
|
||||
}
|
||||
if (ajaxOptionTag == null) return null;
|
||||
|
||||
String datasetId = ajaxOptionTag.getAttributeValue("DATASET");
|
||||
if (datasetId == null) return null;
|
||||
|
||||
PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(myElement.getContainingFile());
|
||||
if (!(ajaxFile instanceof XmlFile xmlFile)) return null;
|
||||
|
||||
XmlTag rootTag = xmlFile.getRootTag();
|
||||
if (rootTag == null) return null;
|
||||
|
||||
XmlTag datasetsContainer = rootTag.findFirstSubTag("DATASETS");
|
||||
if (datasetsContainer == null) datasetsContainer = rootTag;
|
||||
|
||||
for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) {
|
||||
if (datasetId.equals(datasetTag.getAttributeValue("ID"))) {
|
||||
XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS");
|
||||
if (fieldsTag != null) {
|
||||
return findFieldInTag(fieldsTag, fieldName);
|
||||
}
|
||||
return datasetTag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DynFormFileIncludeReference extends PsiReferenceBase<XmlAttributeValue> {
|
||||
private final String path;
|
||||
public DynFormFileIncludeReference(@NotNull XmlAttributeValue element, TextRange range, String path) {
|
||||
super(element, range);
|
||||
this.path = path;
|
||||
}
|
||||
@Nullable @Override public PsiElement resolve() {
|
||||
return DynFormPathUtils.findIncludedFile(myElement.getContainingFile(), path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M32.0845 7.94025V4H24.0203V7.9896H16.029V4H7.91553V7.94025H4V36H16.0044V32.0045C16.0058 30.9457 16.4274 29.9308 17.1766 29.1826C17.9258 28.4345 18.9412 28.0143 20 28.0143C21.0588 28.0143 22.0743 28.4345 22.8234 29.1826C23.5726 29.9308 23.9942 30.9457 23.9956 32.0045V36H36V7.94025H32.0845Z"
|
||||
fill="url(#paint0_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="2.94192" y1="4.89955" x2="37.7772" y2="39.7345" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.15937" stop-color="#3BEA62"/>
|
||||
<stop offset="0.5404" stop-color="#3C99CC"/>
|
||||
<stop offset="0.93739" stop-color="#6B57FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<!-- Form Background and Outline (IntelliJ Blue) -->
|
||||
<rect x="6" y="4" width="28" height="32" rx="3" fill="#389FD6" fill-opacity="0.05" stroke="#389FD6" stroke-width="2"/>
|
||||
|
||||
<!-- Abstract Form Fields (Lines) -->
|
||||
<rect x="10" y="9" width="20" height="1.5" rx="0.75" fill="#389FD6" fill-opacity="0.3"/>
|
||||
<rect x="10" y="13" width="14" height="1.5" rx="0.75" fill="#389FD6" fill-opacity="0.3"/>
|
||||
|
||||
<!-- XML Brackets < > in the Center -->
|
||||
<path d="M15 19L11 23L15 27" stroke="#389FD6" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25 19L29 23L25 27" stroke="#389FD6" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Database Icon at Bottom Right (Action Orange) -->
|
||||
<!-- Base Cylinder -->
|
||||
<path d="M24 28V34C24 35.3807 26.6863 36.5 30 36.5C33.3137 36.5 36 35.3807 36 34V28" fill="#F4AF3D" fill-opacity="0.2"/>
|
||||
<path d="M24 28V34C24 35.3807 26.6863 36.5 30 36.5C33.3137 36.5 36 35.3807 36 34V28" stroke="#F4AF3D" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Top Ellipse -->
|
||||
<ellipse cx="30" cy="28" rx="6" ry="2.5" fill="#F4AF3D" stroke="#F4AF3D" stroke-width="1.5"/>
|
||||
|
||||
<!-- Mid Line of Cylinder -->
|
||||
<path d="M24 31C24 32.3807 26.6863 33.5 30 33.5C33.3137 33.5 36 32.3807 36 31" stroke="#F4AF3D" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user