From 4da00c10e493cf2844b4d20976613ef9f805d03e Mon Sep 17 00:00:00 2001 From: skidus Date: Thu, 16 Apr 2026 19:39:00 +0700 Subject: [PATCH] feat(dynform): enhance dataset and field referencing for master-detail structures - Implemented comprehensive reference and completion support for and tags. - Enhanced dataset resolution to support recursive scanning across included .frml files. - Improved field resolution logic for MASTER-FIELDS and DETAIL-FIELDS to resolve from datasets specified by DATASET-ID or DATAID. - Bumped plugin version to 3.2.3 and updated change notes. --- build.gradle.kts | 10 +- .../dynform/DynFormCompletionContributor.java | 187 +++++++++++++- .../dynform/DynFormReferenceContributor.java | 231 +++++++++++++++++- src/main/resources/META-INF/plugin.xml | 6 + 4 files changed, 420 insertions(+), 14 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f218fe3..95deec7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,12 +3,12 @@ plugins { id("org.jetbrains.kotlin.jvm") version "2.1.0" id("org.jetbrains.intellij.platform") version "2.7.0" } - group = "com.sdk.dynform.tools" -version = "3.2.2" +version = "3.2.3" repositories { mavenCentral() + intellijPlatform { defaultRepositories() } @@ -39,6 +39,12 @@ intellijPlatform { } changeNotes = """ +

[3.2.3]

+
    +
  • Advanced Data Referencing: Implemented comprehensive reference and completion support for <FOREIGN-DATASETS> and <MASTER-DATA> structures.
  • +
  • Contextual Field Resolution: Enhanced field resolution logic to resolve fields from datasets specified by DATASET-ID and DATAID attributes within their respective tags.
  • +
  • Recursive Resource Scanning: Improved dataset and grid scanning to search across recursively included .frml files, ensuring consistent navigation and completion throughout the project.
  • +

[3.2.2]

  • UI/UX Improvement: Updated the I18n settings to allow selecting the message bundle XML file directly via a file browser, improving configuration usability.
  • 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 df9b7c9..38e10ed 100644 --- a/src/main/java/com/sdk/dynform/tools/dynform/DynFormCompletionContributor.java +++ b/src/main/java/com/sdk/dynform/tools/dynform/DynFormCompletionContributor.java @@ -102,19 +102,60 @@ public class DynFormCompletionContributor extends CompletionContributor { } }); - // XML completion for Dataset ID + // XML completion for Dataset ID (DATAID, DATASET, DATASET-ID, VIEW-DATASET) extend(CompletionType.BASIC, XmlPatterns.psiElement() .inside(XmlPatterns.xmlAttributeValue() - .withParent(XmlPatterns.xmlAttribute().withName("DATAID", "DATASET", "DATASET-ID"))), + .withParent(XmlPatterns.xmlAttribute().withName("DATAID", "DATASET", "DATASET-ID", "VIEW-DATASET"))), new CompletionProvider() { @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); + addDatasetsInFileRecursive(parameters.getOriginalFile(), resultSet, new HashSet<>()); + } + }); + + // XML completion for MASTER-FIELDS and DETAIL-FIELDS in MASTER-DATA or FOREIGN-DATASETS > DATASET + extend(CompletionType.BASIC, XmlPatterns.psiElement() + .inside(XmlPatterns.xmlAttributeValue() + .withParent(XmlPatterns.xmlAttribute().withName("MASTER-FIELDS", "DETAIL-FIELDS") + .withParent(XmlPatterns.xmlTag().withName("MASTER-DATA", "DATASET")))), + new CompletionProvider() { + @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; + XmlTag tag = (XmlTag) attr.getParent(); + String tagName = tag.getName(); + String attrName = attr.getName(); + + if ("MASTER-DATA".equals(tagName)) { + if ("MASTER-FIELDS".equals(attrName)) { + addFieldsFromDatasetId(tag, resultSet); + } else if ("DETAIL-FIELDS".equals(attrName)) { + XmlTag gridTag = tag.getParentTag(); + if (gridTag != null && "DATA-GRID".equals(gridTag.getName())) { + addFieldsFromGrid(gridTag, resultSet); + } + } + } else if ("DATASET".equals(tagName) && hasAncestorWithName(tag, "FOREIGN-DATASETS")) { + if ("DETAIL-FIELDS".equals(attrName)) { + addFieldsFromDatasetId(tag, resultSet); + } else if ("MASTER-FIELDS".equals(attrName)) { + 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) { + addFieldsInTagRecursive(fieldsTag, resultSet); + } + } + } + } } } }); @@ -195,6 +236,36 @@ public class DynFormCompletionContributor extends CompletionContributor { } } + private void addDatasetsInFileRecursive(PsiFile file, @NotNull CompletionResultSet resultSet, Set visited) { + if (file == null || !visited.add(file)) 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); + } + + // 3. Add datasets from included files + 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); + addDatasetsInFileRecursive(includedFile, resultSet, visited); + } + } + } + } + } + } + private void addDatasetsInFile(PsiFile file, @NotNull CompletionResultSet resultSet) { if (!(file instanceof XmlFile xmlFile)) return; XmlTag rootTag = xmlFile.getRootTag(); @@ -242,6 +313,110 @@ public class DynFormCompletionContributor extends CompletionContributor { } } + private void addFieldsFromDatasetId(XmlTag tag, @NotNull CompletionResultSet resultSet) { + String datasetId = tag.getAttributeValue("DATASET-ID"); + if (datasetId == null || datasetId.isEmpty()) return; + + PsiFile file = tag.getContainingFile(); + PsiElement datasetElement = findDatasetElement(file, datasetId); + if (datasetElement instanceof XmlTag datasetTag) { + XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS"); + if (fieldsTag != null) { + addFieldsInTagRecursive(fieldsTag, resultSet); + } + } + } + + private void addFieldsFromGrid(XmlTag gridTag, @NotNull CompletionResultSet resultSet) { + // 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 (dataId == null) { + XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR"); + if (editorTag != null) { + dataId = editorTag.getAttributeValue("DATAID"); + } + } + + // 2. Add fields from that DATASET + if (dataId != null && !dataId.isEmpty()) { + PsiElement datasetElement = findDatasetElement(gridTag.getContainingFile(), dataId); + if (datasetElement instanceof XmlTag datasetTag) { + XmlTag fieldsTag = datasetTag.findFirstSubTag("FIELDS"); + if (fieldsTag != null) { + addFieldsInTagRecursive(fieldsTag, resultSet); + } + } + } + + // Fallback: Check for inline FIELDS in GRID-LIST/GRID-EDITOR (if any) + if (listTag != null) { + XmlTag fieldsTag = listTag.findFirstSubTag("FIELDS"); + if (fieldsTag != null) { + addFieldsInTagRecursive(fieldsTag, resultSet); + } + } + XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR"); + if (editorTag != null) { + XmlTag fieldsTag = editorTag.findFirstSubTag("FIELDS"); + if (fieldsTag != null) { + addFieldsInTagRecursive(fieldsTag, resultSet); + } + } + } + + private PsiElement findDatasetElement(PsiFile file, String id) { + return findDatasetInFileRecursiveForCompletion(file, id, new HashSet<>()); + } + + private PsiElement findDatasetInFileRecursiveForCompletion(PsiFile file, String id, Set 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 datasetsContainer = rootTag.findFirstSubTag("DATASETS"); + if (datasetsContainer == null) datasetsContainer = rootTag; + + for (XmlTag datasetTag : datasetsContainer.findSubTags("DATASET")) { + if (id.equals(datasetTag.getAttributeValue("ID"))) { + return datasetTag; + } + } + + PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(file); + if (ajaxFile != null && ajaxFile != file) { + PsiElement found = findDatasetInFileRecursiveForCompletion(ajaxFile, id, visited); + if (found != null) return found; + } + + 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); + if (found != null) return found; + } + } + } + return null; + } + + 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; + } + private PsiNewExpression getNewDynFormExpression(PsiLiteralExpression literal) { PsiElement parent = literal.getParent(); if (parent instanceof PsiExpressionList) { 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 2fe0ef6..350be4a 100644 --- a/src/main/java/com/sdk/dynform/tools/dynform/DynFormReferenceContributor.java +++ b/src/main/java/com/sdk/dynform/tools/dynform/DynFormReferenceContributor.java @@ -83,9 +83,9 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { } }); - // XML Reference for Dataset ID (DATAID, DATASET, DATASET-ID) + // XML Reference for Dataset ID (DATAID, DATASET, DATASET-ID, VIEW-DATASET) registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue() - .withParent(XmlPatterns.xmlAttribute().withName("DATAID", "DATASET", "DATASET-ID")), + .withParent(XmlPatterns.xmlAttribute().withName("DATAID", "DATASET", "DATASET-ID", "VIEW-DATASET")), new PsiReferenceProvider() { @NotNull @Override @@ -97,6 +97,41 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { } }); + // XML Reference for MASTER-FIELDS and DETAIL-FIELDS in MASTER-DATA or FOREIGN-DATASETS > DATASET + registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue() + .withParent(XmlPatterns.xmlAttribute().withName("MASTER-FIELDS", "DETAIL-FIELDS") + .withParent(XmlPatterns.xmlTag().withName("MASTER-DATA", "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; + + XmlAttribute attr = (XmlAttribute) attrValue.getParent(); + XmlTag tag = (XmlTag) attr.getParent(); + String tagName = tag.getName(); + + // Only process DATASET tag if it's inside FOREIGN-DATASETS + if ("DATASET".equals(tagName) && !hasAncestorWithName(tag, "FOREIGN-DATASETS")) { + return PsiReference.EMPTY_ARRAY; + } + + // Support comma-separated fields + String[] fields = value.split(","); + PsiReference[] refs = new PsiReference[fields.length]; + int currentOffset = 1; + for (int i = 0; i < fields.length; i++) { + String field = fields[i].trim(); + int start = value.indexOf(field, currentOffset - 1); + refs[i] = new DynFormMasterDataFieldReference(attrValue, new TextRange(start + 1, start + 1 + field.length()), field); + currentOffset = start + field.length(); + } + return refs; + } + }); + // XML Reference for Grid ID registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue() .withParent(XmlPatterns.xmlAttribute().withName("GRID-ID")), @@ -324,12 +359,39 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { this.datasetId = datasetId; } @Nullable @Override public PsiElement resolve() { - PsiElement found = findDatasetInFile(myElement.getContainingFile(), datasetId); + return findDatasetInFileRecursive(myElement.getContainingFile(), datasetId, new HashSet<>()); + } + + @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; - PsiFile ajaxFile = DynFormPathUtils.findAjaxXml(myElement.getContainingFile()); - if (ajaxFile != null) { - return findDatasetInFile(ajaxFile, datasetId); + // 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; + } + } + } } return null; } @@ -353,6 +415,163 @@ public class DynFormReferenceContributor extends PsiReferenceContributor { } } + private static class DynFormMasterDataFieldReference extends PsiReferenceBase { + private final String fieldName; + + public DynFormMasterDataFieldReference(@NotNull XmlAttributeValue element, TextRange range, String fieldName) { + super(element, range, true); + this.fieldName = fieldName; + } + + @Nullable + @Override + public PsiElement resolve() { + XmlAttribute attr = (XmlAttribute) myElement.getParent(); + XmlTag tag = (XmlTag) attr.getParent(); + String tagName = tag.getName(); + String attrName = attr.getName(); + + 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")); + } 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); + } + } + } 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")); + } 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); + } + } + } + } + } + return null; + } + + @Nullable + private PsiElement resolveInDataset(String datasetId) { + if (datasetId == null || datasetId.isEmpty()) return null; + + // 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); + } + } + 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(); + } + } + if (found instanceof XmlTag) return found; + return null; + } + + @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; + + 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; + } + } + + // 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 (dataId == null) { + XmlTag editorTag = gridTag.findFirstSubTag("GRID-EDITOR"); + 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); + } + } + return null; + } + } + private static class DynFormGridReference extends PsiReferenceBase { private final String gridId; public DynFormGridReference(@NotNull XmlAttributeValue element, TextRange range, String gridId) { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7e06c8c..86096ae 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -34,6 +34,12 @@ ]]> [3.2.3] +
      +
    • Advanced Data Referencing: Implemented comprehensive reference and completion support for <FOREIGN-DATASETS> and <MASTER-DATA> structures.
    • +
    • Contextual Field Resolution: Enhanced field resolution logic to resolve fields from datasets specified by DATASET-ID and DATAID attributes within their respective tags.
    • +
    • Recursive Resource Scanning: Improved dataset and grid scanning to search across recursively included .frml files, ensuring consistent navigation and completion throughout the project.
    • +

    [3.2.2]

    • UI/UX Improvement: Updated the I18n settings to allow selecting the message bundle XML file directly via a file browser, improving configuration usability.