feat(dynform): reference and completion for FORM-NAME attribute

- Added comprehensive reference and completion support for FORM-NAME in DATASET > FIELDS > FIELD.
- Implemented smart field resolution to target hidden fields in FORM_ENTRY > FIELDS and named fields in FORM_ENTRY > LAYOUT.
- Enhanced form scanning to recursively discover fields across all included .frml files.
- Bumped plugin version to 3.2.5.
This commit is contained in:
2026-04-18 12:24:12 +07:00
parent 475555da83
commit d2d2d4d642
4 changed files with 161 additions and 1 deletions

View File

@@ -213,6 +213,22 @@ public class DynFormCompletionContributor extends CompletionContributor {
addGridsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>());
}
});
// XML completion for FORM-NAME in DATASET > FIELDS > FIELD
extend(CompletionType.BASIC, XmlPatterns.psiElement()
.inside(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("FORM-NAME")
.withParent(XmlPatterns.xmlTag().withName("FIELD")
.withParent(XmlPatterns.xmlTag().withName("FIELDS")
.withParent(XmlPatterns.xmlTag().withName("DATASET")))))),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context,
@NotNull CompletionResultSet resultSet) {
addFormFieldsForNameRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>());
}
});
}
private void addFieldsInTagRecursive(XmlTag container, @NotNull CompletionResultSet resultSet) {
@@ -438,4 +454,60 @@ public class DynFormCompletionContributor extends CompletionContributor {
}
return null;
}
private void addFormFieldsForNameRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set<PsiFile> visited) {
if (file == null || !visited.add(file)) return;
if (!(file instanceof XmlFile xmlFile)) return;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return;
// 1. Search in current file FORM_ENTRY tags
for (XmlTag formTag : rootTag.findSubTags("FORM")) {
for (XmlTag entryTag : formTag.findSubTags("FORM_ENTRY")) {
// Collect hidden fields from FIELDS
XmlTag fieldsTag = entryTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
for (XmlTag fieldTag : fieldsTag.findSubTags("FIELD")) {
if ("HIDDEN".equals(fieldTag.getAttributeValue("INPUTTYPE"))) {
String name = fieldTag.getAttributeValue("NAME");
if (name != null && !name.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(name)
.withIcon(com.intellij.icons.AllIcons.Nodes.Field)
.withTypeText("Hidden Field"));
}
}
}
}
// Collect any field/tag with NAME from LAYOUT
XmlTag layoutTag = entryTag.findFirstSubTag("LAYOUT");
if (layoutTag != null) {
addFieldsInLayoutRecursive(layoutTag, resultSet);
}
}
}
// 2. Search in included files
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
addFormFieldsForNameRecursive(includedFile, resultSet, visited);
}
}
}
}
private void addFieldsInLayoutRecursive(XmlTag container, @NotNull CompletionResultSet resultSet) {
for (XmlTag subTag : container.getSubTags()) {
String name = subTag.getAttributeValue("NAME");
if (name != null && !name.isEmpty()) {
resultSet.addElement(LookupElementBuilder.create(name)
.withIcon(com.intellij.icons.AllIcons.Nodes.Field)
.withTypeText("Layout: " + subTag.getName()));
}
addFieldsInLayoutRecursive(subTag, resultSet);
}
}
}

View File

@@ -146,6 +146,23 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
}
});
// XML Reference for FORM-NAME in DATASET > FIELDS > FIELD
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("FORM-NAME")
.withParent(XmlPatterns.xmlTag().withName("FIELD")
.withParent(XmlPatterns.xmlTag().withName("FIELDS")
.withParent(XmlPatterns.xmlTag().withName("DATASET"))))),
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 DynFormFormNameReference(attrValue, new TextRange(1, value.length() + 1), value)};
}
});
// XML Reference for SRC in UPDATE-FIELDS
registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
.withParent(XmlPatterns.xmlAttribute().withName("SRC")
@@ -662,4 +679,63 @@ public class DynFormReferenceContributor extends PsiReferenceContributor {
return DynFormPathUtils.findIncludedFile(myElement.getContainingFile(), path);
}
}
private static class DynFormFormNameReference extends PsiReferenceBase<XmlAttributeValue> {
private final String fieldName;
public DynFormFormNameReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) {
super(element, range, true);
this.fieldName = fieldName;
}
@Nullable
@Override
public PsiElement resolve() {
return findFieldInFormEntriesRecursive(myElement.getContainingFile(), fieldName, new HashSet<>());
}
@Nullable
private PsiElement findFieldInFormEntriesRecursive(PsiFile file, String name, Set<PsiFile> visited) {
if (file == null || !visited.add(file)) return null;
if (!(file instanceof XmlFile xmlFile)) return null;
XmlTag rootTag = xmlFile.getRootTag();
if (rootTag == null) return null;
// 1. Search in current file FORM_ENTRY tags
for (XmlTag formTag : rootTag.findSubTags("FORM")) {
for (XmlTag entryTag : formTag.findSubTags("FORM_ENTRY")) {
// Check hidden fields in FIELDS
XmlTag fieldsTag = entryTag.findFirstSubTag("FIELDS");
if (fieldsTag != null) {
for (XmlTag fieldTag : fieldsTag.findSubTags("FIELD")) {
if ("HIDDEN".equals(fieldTag.getAttributeValue("INPUTTYPE")) && name.equals(fieldTag.getAttributeValue("NAME"))) {
XmlAttribute nameAttr = fieldTag.getAttribute("NAME");
return nameAttr != null ? nameAttr.getValueElement() : fieldTag;
}
}
}
// Check fields in LAYOUT
XmlTag layoutTag = entryTag.findFirstSubTag("LAYOUT");
if (layoutTag != null) {
PsiElement found = findFieldInTag(layoutTag, name);
if (found != null) return found;
}
}
}
// 2. Search in included files
XmlTag includesTag = rootTag.findFirstSubTag("INCLUDES");
if (includesTag != null) {
for (XmlTag includeTag : includesTag.findSubTags("INCLUDE")) {
String path = includeTag.getAttributeValue("FILE");
if (path != null) {
PsiFile includedFile = DynFormPathUtils.findIncludedFile(file, path);
PsiElement found = findFieldInFormEntriesRecursive(includedFile, name, visited);
if (found != null) return found;
}
}
}
return null;
}
}
}

View File

@@ -34,6 +34,12 @@
]]></description>
<change-notes><![CDATA[
<h2>[3.2.5]</h2>
<ul>
<li><strong>Dataset to Form Mapping:</strong> Implemented comprehensive reference and completion support for the <code>FORM-NAME</code> attribute in <code>DATASET > FIELDS > FIELD</code>.</li>
<li><strong>Smart Field Resolution:</strong> <code>FORM-NAME</code> now correctly resolves to hidden fields in <code>FORM_ENTRY > FIELDS</code> and any named fields within <code>FORM_ENTRY > LAYOUT</code>.</li>
<li><strong>Recursive Form Scanning:</strong> Enhanced field discovery to scan across all form entries in the current and recursively included <code>.frml</code> files.</li>
</ul>
<h2>[3.2.4]</h2>
<ul>
<li><strong>Persistent Generation Directories:</strong> Generator now remembers the last used directory for Action Beans and Dataset XMLs independently per project.</li>