From d0c64523380c97a3987d273294bb9c5d62e8df25 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 17 Jun 2025 21:52:06 +0800 Subject: [PATCH 01/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B=20XL?= =?UTF-8?q?angDocumentationProvider=20=E7=9A=84=E4=BB=A3=E7=A0=81=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E4=B8=BA=E5=85=B6=E8=A1=A5=E5=85=85=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=20TestXLangDocumentationProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc/XLangDocumentationProvider.java | 159 +++++++++--------- .../idea/plugin/services/NopAppListener.java | 1 + .../idea/plugin/BaseXLangPluginTestCase.java | 66 ++++++++ .../doc/TestXLangDocumentationProvider.java | 57 +++++++ .../_vfs/dict/test/doc/child-type.dict.yaml | 5 + .../test/resources/_vfs/test/doc/example.xdef | 13 ++ 6 files changed, 221 insertions(+), 80 deletions(-) create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java create mode 100644 nop-idea-plugin/src/test/resources/_vfs/dict/test/doc/child-type.dict.yaml create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java index cc42cb038..d4b8c23e4 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java @@ -7,6 +7,8 @@ */ package io.nop.idea.plugin.doc; +import java.util.Objects; + import com.intellij.lang.documentation.AbstractDocumentationProvider; import com.intellij.psi.PsiElement; import com.intellij.psi.util.PsiTreeUtil; @@ -16,12 +18,7 @@ import com.intellij.psi.xml.XmlTokenType; import io.nop.api.core.beans.DictBean; import io.nop.api.core.beans.DictOptionBean; import io.nop.commons.util.StringHelper; -import io.nop.core.dict.DictModel; -import io.nop.core.dict.DictModelParser; import io.nop.core.dict.DictProvider; -import io.nop.core.resource.IResource; -import io.nop.core.resource.VirtualFileSystem; -import io.nop.core.resource.component.ResourceComponentManager; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.MarkdownHelper; import io.nop.idea.plugin.utils.XDefPsiHelper; @@ -31,91 +28,29 @@ import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefComment; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.XDefTypeDecl; -import org.jetbrains.annotations.NotNull; - import jakarta.annotation.Nullable; -import java.util.Objects; +import org.jetbrains.annotations.NotNull; public class XLangDocumentationProvider extends AbstractDocumentationProvider { @Override - public @Nullable - String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { + public @Nullable String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { return ProjectEnv.withProject(element.getProject(), () -> { - return doGenerate(element, originalElement); - }); - } - - String doGenerate(PsiElement element, PsiElement elm) { - if (XmlPsiHelper.isElementType(elm, XmlTokenType.XML_NAME)) { - PsiElement parent = elm.getParent(); - - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(parent); - if (tagInfo == null || tagInfo.getDefNode() == null) { - return null; - } - - if (parent instanceof XmlTag) { - DocInfo doc = new DocInfo(tagInfo.getDefNode()); - - IXDefComment comment = tagInfo.getDefNode().getComment(); - if (comment != null) { - doc.setTitle(comment.getMainDisplayName()); - doc.setDesc(comment.getMainDescription()); - } - return doc.toString(); - } else if (parent instanceof XmlAttribute) { - XmlAttribute attr = (XmlAttribute) parent; - String attrName = attr.getName(); - DocInfo doc = new DocInfo(tagInfo.getDefNode().getAttribute(attrName)); - - IXDefComment comment = tagInfo.getDefNode().getComment(); - if (comment != null) { - doc.setTitle(comment.getSubDisplayName(attrName)); - doc.setDesc(comment.getSubDescription(attrName)); - } - return doc.toString(); - } - } else if (XmlPsiHelper.isElementType(elm, XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)) { - XmlAttribute attr = PsiTreeUtil.getParentOfType(elm, XmlAttribute.class); - if (attr == null) { - return null; - } - - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); - if (tagInfo == null || tagInfo.getDefNode() == null) { - return null; - } - - XDefTypeDecl defType = tagInfo.getDefNode().getAttrType(attr.getName()); - if (defType == null || defType.getOptions() == null) { - return null; - } - - DictBean dictBean = DictProvider.instance().getDict(null, defType.getOptions(), null, null); - DictOptionBean option = dictBean != null ? dictBean.getOptionByValue(attr.getValue()) : null; - if (option == null) { - return null; - } - - DocInfo doc = new DocInfo(); - if (!Objects.equals(option.getLabel(), option.getValue())) { - doc.setTitle(option.getLabel()); + if (XmlPsiHelper.isElementType(originalElement, XmlTokenType.XML_NAME)) { + return generateDocForXmlName(originalElement); + } // + else if (XmlPsiHelper.isElementType(originalElement, XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)) { + return generateDocForXmlAttributeValue(originalElement); } - doc.setDesc(option.getDescription()); - - return doc.toString(); - } - - return null; + return null; + }); } /** * Provides documentation when a Simple Language element is hovered with the mouse. */ @Override - public @Nullable - String generateHoverDoc(@NotNull PsiElement element, @Nullable PsiElement originalElement) { + public @Nullable String generateHoverDoc(@NotNull PsiElement element, @Nullable PsiElement originalElement) { return generateDoc(element, originalElement); } @@ -125,8 +60,7 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { * Provides the information in which file the Simple language key/value is defined. */ @Override - public @Nullable - String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { + public @Nullable String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { // if (element instanceof SimpleProperty) { // final String key = ((SimpleProperty) element).getKey(); // final String file = SymbolPresentationUtil.getFilePathPresentation(element.getContainingFile()); @@ -135,9 +69,74 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { return null; } + /** 为 xml 标签名和属性名生成文档 */ + private String generateDocForXmlName(PsiElement element) { + PsiElement parent = element.getParent(); + + XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(parent); + if (tagInfo == null || tagInfo.getDefNode() == null) { + return null; + } + + if (parent instanceof XmlTag) { + DocInfo doc = new DocInfo(tagInfo.getDefNode()); + + IXDefComment comment = tagInfo.getDefNode().getComment(); + if (comment != null) { + doc.setTitle(comment.getMainDisplayName()); + doc.setDesc(comment.getMainDescription()); + } + return doc.toString(); + } else if (parent instanceof XmlAttribute attr) { + String attrName = attr.getName(); + DocInfo doc = new DocInfo(tagInfo.getDefNode().getAttribute(attrName)); + + IXDefComment comment = tagInfo.getDefNode().getComment(); + if (comment != null) { + doc.setTitle(comment.getSubDisplayName(attrName)); + doc.setDesc(comment.getSubDescription(attrName)); + } + return doc.toString(); + } + + return null; + } + + /** 为 xml 属性值生成文档 */ + private String generateDocForXmlAttributeValue(PsiElement element) { + XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); + if (attr == null) { + return null; + } + + XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); + if (tagInfo == null || tagInfo.getDefNode() == null) { + return null; + } + + XDefTypeDecl defType = tagInfo.getDefNode().getAttrType(attr.getName()); + if (defType == null || defType.getOptions() == null) { + return null; + } + + DictBean dictBean = DictProvider.instance().getDict(null, defType.getOptions(), null, null); + DictOptionBean option = dictBean != null ? dictBean.getOptionByValue(attr.getValue()) : null; + if (option == null) { + return null; + } + + DocInfo doc = new DocInfo(); + if (!Objects.equals(option.getLabel(), option.getValue())) { + doc.setTitle(option.getLabel()); + } + doc.setDesc(option.getDescription()); + + return doc.toString(); + } + /** 对于多行文本,行首的 > 将被去除后,再按照 markdown 渲染得到 html 代码 */ public static String markdown(String text) { - text = text.replaceAll("(?m)^> ",""); + text = text.replaceAll("(?m)^> ", ""); text = MarkdownHelper.renderHtml(text); return text; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java index e37efe584..7c7a197cb 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java @@ -24,6 +24,7 @@ import java.util.List; public class NopAppListener implements AppLifecycleListener { + @Override public void appFrameCreated(@NotNull List commandLineArgs) { AppConfig.getConfigProvider().updateConfigValue(ApiConfigs.CFG_DEBUG, false); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java new file mode 100644 index 000000000..d44e0a3bc --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -0,0 +1,66 @@ +package io.nop.idea.plugin; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import io.nop.api.core.ApiConfigs; +import io.nop.api.core.config.AppConfig; +import io.nop.commons.lang.impl.Cancellable; +import io.nop.core.dict.DictProvider; +import io.nop.core.initialize.ICoreInitializer; +import io.nop.core.initialize.impl.ReflectionHelperMethodInitializer; +import io.nop.core.initialize.impl.VirtualFileSystemInitializer; +import io.nop.idea.plugin.lang.XLangFileType; +import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.xlang.initialize.XLangCoreInitializer; + +/** + * @author flytreeleft + * @date 2025-06-17 + */ +public abstract class BaseXLangPluginTestCase extends BasePlatformTestCase { + private final Cancellable cleanup = new Cancellable(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // Note: 消除异常 "Write access is allowed inside write-action only" + ApplicationManager.getApplication().runWriteAction(() -> { + // 临时注册 XLang 文件类型 + for (String ext : getXLangFileExtensions()) { + FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, ext); + } + }); + + // 初始化 XLang 环境:由于测试资源均在 classpath 中,故而,需采用默认的 ICoreInitializer 进行初始化, + // 而不能通过 NopAppListener 初始化 + ProjectEnv.withProject(getProject(), () -> { + AppConfig.getConfigProvider().updateConfigValue(ApiConfigs.CFG_DEBUG, false); + + ICoreInitializer[] initializers = new ICoreInitializer[] { + new XLangCoreInitializer(), + new VirtualFileSystemInitializer(), + new ReflectionHelperMethodInitializer(), + }; + for (ICoreInitializer initializer : initializers) { + initializer.initialize(); + cleanup.appendOnCancelTask(initializer::destroy); + } + + cleanup.append(DictProvider.registerLoader()); + + return null; + }); + } + + @Override + protected void tearDown() throws Exception { + cleanup.cancel(); + super.tearDown(); + } + + protected String[] getXLangFileExtensions() { + return new String[0]; + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java new file mode 100644 index 000000000..b51e38d4f --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -0,0 +1,57 @@ +package io.nop.idea.plugin.doc; + +import com.intellij.codeInsight.documentation.DocumentationManager; +import com.intellij.lang.documentation.DocumentationProvider; +import com.intellij.psi.PsiElement; +import io.nop.idea.plugin.BaseXLangPluginTestCase; + +/** + * @author flytreeleft + * @date 2025-06-17 + */ +public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { + private static final String XLANG_EXT = "xdoc"; + + @Override + protected String[] getXLangFileExtensions() { + return new String[] { XLANG_EXT }; + } + + public void testGenerateDocForXmlName() { + doTest("ple xmlns:x=\"/nop/schema/xdsl.xdef\" x:schema=\"/test/doc/example.xdef\">" + + " " + + "", "

This is root node

\n"); + + doTest("" + + " ild name=\"Child\"/>" + + "", "

This is child node

\n"); + + doTest("" + + " me=\"Child\"/>" + + "", "

stdDomain=string



This is child name

\n"); + } + + public void testGenerateDocForXmlAttributeValue() { + doTest("" + + " af\"/>" + + "", "

leaf-Leaf Node

"); + } + + /** 通过在 text 中插入 <caret> 代表光标位置 */ + private void doTest(String text, String doc) { + myFixture.configureByText("example." + XLANG_EXT, text); + + // Note: 通过 ApplicationManager.getApplication().runReadAction(() -> {}) + // 消除异常 "Read access is allowed from inside read-action" + PsiElement originalElement = myFixture.getFile() + .findElementAt(myFixture.getEditor().getCaretModel().getOffset()); + PsiElement element = DocumentationManager.getInstance(getProject()) + .findTargetElement(myFixture.getEditor(), myFixture.getFile()); + + DocumentationProvider docProvider = DocumentationManager.getProviderFromElement(originalElement); + String genDoc = docProvider.generateDoc(element, originalElement); + + assertEquals(doc, genDoc); + } +} + diff --git a/nop-idea-plugin/src/test/resources/_vfs/dict/test/doc/child-type.dict.yaml b/nop-idea-plugin/src/test/resources/_vfs/dict/test/doc/child-type.dict.yaml new file mode 100644 index 000000000..5f3653c9b --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/dict/test/doc/child-type.dict.yaml @@ -0,0 +1,5 @@ +options: + - value: node + label: Normal Node + - value: leaf + label: Leaf Node diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef new file mode 100644 index 000000000..cd93dffce --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -0,0 +1,13 @@ + + + + + + + -- Gitee From 2444e6ba905338c677926ab2d2d5b617882e2c82 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 18 Jun 2025 09:05:35 +0800 Subject: [PATCH 02/82] =?UTF-8?q?nop-idea-pugin:=20=E4=BD=BF=E7=94=A8=20Ja?= =?UTF-8?q?va=2015+=20=E7=9A=84=E6=96=87=E6=9C=AC=E5=9D=97=E8=AF=AD?= =?UTF-8?q?=E6=B3=95=E7=BC=96=E5=86=99=E5=A4=9A=E8=A1=8C=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc/TestXLangDocumentationProvider.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index b51e38d4f..3575c08bb 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -6,6 +6,8 @@ import com.intellij.psi.PsiElement; import io.nop.idea.plugin.BaseXLangPluginTestCase; /** + * 参考 https://github.com/JetBrains/intellij-community/blob/master/xml/tests/src/com/intellij/html/HtmlDocumentationTest.java + * * @author flytreeleft * @date 2025-06-17 */ @@ -18,23 +20,31 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { } public void testGenerateDocForXmlName() { - doTest("ple xmlns:x=\"/nop/schema/xdsl.xdef\" x:schema=\"/test/doc/example.xdef\">" - + " " - + "", "

This is root node

\n"); + doTest(""" + ple xmlns:x="/nop/schema/xdsl.xdef" x:schema="/test/doc/example.xdef"> + + + """, "

This is root node

\n"); - doTest("" - + " ild name=\"Child\"/>" - + "", "

This is child node

\n"); + doTest(""" + + ild name="Child"/> + + """, "

This is child node

\n"); - doTest("" - + " me=\"Child\"/>" - + "", "

stdDomain=string



This is child name

\n"); + doTest(""" + + me="Child"/> + + """, "

stdDomain=string



This is child name

\n"); } public void testGenerateDocForXmlAttributeValue() { - doTest("" - + " af\"/>" - + "", "

leaf-Leaf Node

"); + doTest(""" + + + + """, "

leaf-Leaf Node

"); } /** 通过在 text 中插入 <caret> 代表光标位置 */ -- Gitee From 74a03efbca9862896fc6100271609fa625b307c4 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 18 Jun 2025 21:57:18 +0800 Subject: [PATCH 03/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E4=BB=8E=20xml=20=E5=B1=9E=E6=80=A7=E5=80=BC=E4=B8=AD=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E8=B7=B3=E8=BD=AC=E6=96=87=E4=BB=B6=E7=9A=84=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E9=80=9A=E8=BF=87=E5=B1=9E=E6=80=A7=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=98=AF=E5=90=A6=E4=B8=BA=20v-path=E3=80=81v-path-li?= =?UTF-8?q?st=20=E5=86=B3=E5=AE=9A=E6=98=AF=E5=90=A6=E5=81=9A=20vfs=20?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E6=96=87=E4=BB=B6=E7=9A=84=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=EF=BC=8C=E4=BB=8E=E8=80=8C=E5=81=9A=E6=9B=B4=E5=8A=A0=E5=87=86?= =?UTF-8?q?=E7=A1=AE=E7=9A=84=E5=88=A4=E6=96=AD=E5=B9=B6=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=AF=B9=E7=9B=B8=E5=85=B3=E5=B1=9E=E6=80=A7=E7=9A=84=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../link/XLangFileDeclarationHandler.java | 132 ---------- .../link/XLangGotoDeclarationHandler.java | 248 ++++++++++++++++++ .../nop/idea/plugin/utils/XDefPsiHelper.java | 66 +++-- .../io/nop/idea/plugin/utils/XmlTagInfo.java | 38 ++- .../src/main/resources/META-INF/plugin.xml | 2 +- .../idea/plugin/BaseXLangPluginTestCase.java | 21 +- .../link/TestXLangGotoDeclarationHandler.java | 139 ++++++++++ .../idea/plugin/utils/TestXDefPsiHelper.java | 17 ++ .../src/test/resources/_vfs/test/link/a.xmeta | 2 + .../src/test/resources/_vfs/test/link/b.xmeta | 2 + .../resources/_vfs/test/link/default.xform | 2 + 11 files changed, 501 insertions(+), 168 deletions(-) delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangFileDeclarationHandler.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestXDefPsiHelper.java create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/link/a.xmeta create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/link/b.xmeta create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/link/default.xform diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangFileDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangFileDeclarationHandler.java deleted file mode 100644 index 61cf27f7d..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangFileDeclarationHandler.java +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ -package io.nop.idea.plugin.link; - -import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandlerBase; -import com.intellij.lang.ASTNode; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiElement; -import com.intellij.psi.tree.IElementType; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlElementType; -import com.intellij.psi.xml.XmlTag; -import io.nop.commons.util.StringHelper; -import io.nop.idea.plugin.utils.XmlPsiHelper; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -/*** - * 点击ctrl能够链接到lib文件 - */ -public class XLangFileDeclarationHandler extends GotoDeclarationHandlerBase { - - - @Override - public PsiElement @Nullable [] getGotoDeclarationTargets(@Nullable PsiElement sourceElement, int offset, Editor editor) { - PsiElement element = sourceElement.getParent(); - if (element == null) return null; - -// if (sourceElement.getContainingFile().getFileType() != XLangFileType.INSTANCE) -// return null; - - ASTNode node = element.getNode(); - IElementType nodeType = node.getElementType(); - if (nodeType == XmlElementType.XML_TAG) { - XmlTag tag = (XmlTag) element; - String tagName = tag.getName(); - if (isCustomTag(tagName)) { - return XmlPsiHelper.findXplTag(editor.getProject(), tag); - } - } else if (nodeType == XmlElementType.XML_ATTRIBUTE_VALUE) { - XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); - if (attr == null) - return null; - - String name = attr.getName(); - String value = attr.getValue(); - if (!StringHelper.isEmpty(value)) { - if (name.equals("xpl:lib")) { - List paths = StringHelper.split(value, ','); - if (paths.size() == 1) { - Project project = editor.getProject(); - String path = XmlPsiHelper.absolutePath(value, attr); - return XmlPsiHelper.findPsiFile(project, path); - } - // @TODO 对于同时引入多个库的情况,暂时没有处理 - } else if (isVfsPath(attr)) { - Project project = editor.getProject(); - String path = XmlPsiHelper.absolutePath(value, attr); - return XmlPsiHelper.findPsiFile(project, path); - } - } - } else if (nodeType == XmlElementType.XML_TEXT) { - if (element.getParent() instanceof XmlTag) { - XmlTag parent = (XmlTag) element.getParent(); - String text = element.getText().trim(); - if ((text.indexOf('.') > 0 || text.indexOf('/') > 0) && StringHelper.isValidFilePath(text)) { - Project project = editor.getProject(); - String path = XmlPsiHelper.absolutePath(text, parent); - return XmlPsiHelper.findPsiFile(project, path); - } - } - } - - return null; - } - - private boolean isCustomTag(String tagName) { - int pos = tagName.indexOf(':'); - if (pos <= 0) - return false; - String ns = tagName.substring(0, pos); - - // 内置的名字空间 - if (ns.equals("x") || ns.equals("xdef") || ns.equals("xdsl") || ns.equals("xpl") - || ns.equals("c") || ns.equals("macro") || ns.equals("xmlns")) - return false; - return true; - } - - private boolean isVfsPath(XmlAttribute attr) { - String value = attr.getValue(); - String name = attr.getName(); - - if (StringHelper.isEmpty(value)) - return false; - - if (value.indexOf(':') >= 0) - return false; - - if ("x:extends".equals(name)) - return true; - - if ("x:schema".equals(name)) - return true; - - if (name.startsWith("xmlns:") && value.endsWith(".xdef")) - return true; - - if (name.equals("xdef:ref") || name.equals("ref")) { - if (value.indexOf('.') > 0) - return true; - } - - // - // - return StringHelper.isValidFilePath(value); - } - - @Override - public @Nullable PsiElement getGotoDeclarationTarget(@Nullable PsiElement sourceElement, Editor editor) { - PsiElement[] elements = getGotoDeclarationTargets(sourceElement, 0, editor); - return elements.length == 0 ? null : elements[0]; - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java new file mode 100644 index 000000000..4d9d3dd61 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -0,0 +1,248 @@ +/** + * Copyright (c) 2017-2024 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ +package io.nop.idea.plugin.link; + +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandlerBase; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlElement; +import com.intellij.psi.xml.XmlElementType; +import com.intellij.psi.xml.XmlTag; +import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.utils.XDefPsiHelper; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.utils.XmlTagInfo; +import io.nop.xlang.xdef.XDefConstants; +import io.nop.xlang.xdef.XDefTypeDecl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/*** + * 点击ctrl能够链接到lib文件 + */ +public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { + + @Override + public @Nullable PsiElement getGotoDeclarationTarget(@Nullable PsiElement sourceElement, Editor editor) { + PsiElement[] elements = getGotoDeclarationTargets(sourceElement, 0, editor); + return elements.length == 0 ? null : elements[0]; + } + + @Override + public @Nullable PsiElement[] getGotoDeclarationTargets( + @Nullable PsiElement sourceElement, int offset, Editor editor + ) { + Project project = editor.getProject(); + PsiElement element = sourceElement != null ? sourceElement.getParent() : null; + + if (XmlPsiHelper.isElementType(element, XmlElementType.XML_TAG)) { + return getGotoDeclarationTargetsForXmlTag(project, element); + } // + else if (XmlPsiHelper.isElementType(element, XmlElementType.XML_ATTRIBUTE_VALUE)) { + return getGotoDeclarationTargetsForXmlAttributeValue(project, element, offset); + } // + else if (XmlPsiHelper.isElementType(element, XmlElementType.XML_TEXT)) { + return getGotoDeclarationTargetsForXmlText(project, element); + } + + return null; + } + + /** 获取可从 xml 标签上跳转的元素(文件路径、节点引用等) */ + private PsiElement[] getGotoDeclarationTargetsForXmlTag(Project project, PsiElement element) { + XmlTag tag = (XmlTag) element; + String tagName = tag.getName(); + + if (!isCustomTag(tagName)) { + return null; + } + return XmlPsiHelper.findXplTag(project, tag); + } + + /** 获取可从 xml 属性值中跳转的元素(文件路径、节点引用等) */ + private PsiElement[] getGotoDeclarationTargetsForXmlAttributeValue( + Project project, PsiElement element, int cursorOffset + ) { + XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); + if (attr == null) { + return null; + } + + String attrValue = attr.getValue(); + if (StringHelper.isEmpty(attrValue)) { + return null; + } + + XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); + + XDefTypeDecl attrDefType = null; + if (tagInfo != null && tagInfo.getDefNode() != null) { + attrDefType = tagInfo.getAttrType(attr.getName()); + } + + if (attrDefType == null) { + // 未定义类型属性,直接针对 *.xdef 文件做跳转 + if (attrValue.endsWith(".xdef") && attrValue.contains("/")) { + return getPsiFilesFromPathCsv(project, attr.getContainingFile(), cursorOffset); + } + return null; + } + + String stdDomain = attrDefType.getStdDomain(); + if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain)) { + return getPsiFilesByPath(project, attr, attrValue); + } // + else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { + return getPsiFilesFromPathCsv(project, attr.getContainingFile(), cursorOffset); + } + + // XDefConstants.STD_DOMAIN_METHOD_REF + // XDefConstants.STD_DOMAIN_NAME_OR_V_PATH + // XDefConstants.STD_DOMAIN_XDEF_REF + +// if (attrName.equals("xpl:lib")) { +// String path = XmlPsiHelper.absolutePath(attrValue, attr); +// +// return XmlPsiHelper.findPsiFile(project, path); +// } else if (isVfsPath(attr)) { +// String path = XmlPsiHelper.absolutePath(attrValue, attr); +// +// return XmlPsiHelper.findPsiFile(project, path); +// } + + return null; + } + + /** 获取可从 xml 文本中跳转的元素(文件路径、节点引用等) */ + private PsiElement[] getGotoDeclarationTargetsForXmlText(Project project, PsiElement element) { + if (!(element.getParent() instanceof XmlTag parent)) { + return null; + } + + String text = element.getText().trim(); + if ((text.indexOf('.') > 0 || text.indexOf('/') > 0) // + && StringHelper.isValidFilePath(text) // + ) { + String path = XmlPsiHelper.absolutePath(text, parent); + + return XmlPsiHelper.findPsiFile(project, path); + } + + return null; + } + + /** 获取指定路径下的文件 */ + private PsiElement[] getPsiFilesByPath(Project project, @NotNull XmlElement element, String path) { + if (!StringHelper.isValidFilePath(path)) { + return null; + } + + path = XmlPsiHelper.absolutePath(path, element); + + return XmlPsiHelper.findPsiFile(project, path); + } + + /** 从 csv 文本中取光标处的文件 */ + private PsiElement[] getPsiFilesFromPathCsv(Project project, @NotNull PsiFile file, int cursorOffset) { + PsiElement element = file.findElementAt(cursorOffset); + assert element != null; + + // 计算 光标所在元素 在文件中的绝对位置 + int elementStart = 0; + PsiElement parent = element; + while (parent != null && parent.getStartOffsetInParent() > 0) { + elementStart += parent.getStartOffsetInParent(); + parent = parent.getParent(); + } + + String path = extractPathFromCsv(element.getText(), cursorOffset - elementStart); + + return getPsiFilesByPath(project, (XmlElement) element, path); + } + + /** 从 csv 中提取指定偏移位置所在的文件路径 */ + private String extractPathFromCsv(String csv, int offset) { + int start = offset; + int end = offset; + + while (start > 0) { + char ch = csv.charAt(start - 1); + if (ch != ',' && !Character.isWhitespace(ch)) { + start -= 1; + } else { + break; + } + } + while (end < csv.length()) { + char ch = csv.charAt(end); + if (ch != ',' && !Character.isWhitespace(ch)) { + end += 1; + } else { + break; + } + } + + return csv.substring(start, end); + } + + private boolean isCustomTag(String tagName) { + int pos = tagName.indexOf(':'); + if (pos <= 0) { + return false; + } + + // 内置的名字空间 + String ns = tagName.substring(0, pos); + return !ns.equals("x") + && !ns.equals("xdef") + && !ns.equals("xdsl") + && !ns.equals("xpl") + && !ns.equals("c") + && !ns.equals("macro") + && !ns.equals("xmlns"); + } + + private boolean isVfsPath(XmlAttribute attr) { + String value = attr.getValue(); + String name = attr.getName(); + + if (StringHelper.isEmpty(value)) { + return false; + } + + if (value.indexOf(':') >= 0) { + return false; + } + + if ("x:extends".equals(name)) { + return true; + } + + if ("x:schema".equals(name)) { + return true; + } + + if (name.startsWith("xmlns:") && value.endsWith(".xdef")) { + return true; + } + + if (name.equals("xdef:ref") || name.equals("ref")) { + if (value.indexOf('.') > 0) { + return true; + } + } + + // + // + return StringHelper.isValidFilePath(value); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index e8e514200..cf606c222 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -7,6 +7,9 @@ */ package io.nop.idea.plugin.utils; +import java.util.ArrayList; +import java.util.List; + import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.xml.XmlTag; @@ -22,9 +25,6 @@ import io.nop.xlang.xmeta.SchemaLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - import static io.nop.idea.plugin.utils.XmlPsiHelper.getXmlTag; public class XDefPsiHelper { @@ -34,32 +34,30 @@ public class XDefPsiHelper { private static IXDefinition xdslDef; private static IXDefinition xplDef; - public static synchronized IXDefinition getXdefDef() { if (xdefDef == null) { - IXDefinition xdef = new XDefinitionParser().parseFromResource(new ClassPathResource("classpath:/_vfs/nop/schema/xdef.xdef")); - xdefDef = xdef; + xdefDef = new XDefinitionParser().parseFromResource(new ClassPathResource( + "classpath:/_vfs/nop/schema/xdef.xdef")); } return xdefDef; } public static synchronized IXDefinition getXDslDef() { if (xdslDef == null) { - IXDefinition xdef = new XDefinitionParser().parseFromResource(new ClassPathResource("classpath:/_vfs/nop/schema/xdsl.xdef")); - xdslDef = xdef; + xdslDef = new XDefinitionParser().parseFromResource(new ClassPathResource( + "classpath:/_vfs/nop/schema/xdsl.xdef")); } return xdslDef; } public static synchronized IXDefinition getXplDef() { if (xplDef == null) { - IXDefinition xdef = new XDefinitionParser().parseFromResource(new ClassPathResource("classpath:/_vfs/nop/schema/xpl.xdef")); - xplDef = xdef; + xplDef = new XDefinitionParser().parseFromResource(new ClassPathResource( + "classpath:/_vfs/nop/schema/xpl.xdef")); } return xplDef; } - public static String getSchemaPath(XmlTag tag) { PsiFile file = tag.getContainingFile(); String fileExt = StringHelper.fileExt(file.getName()); @@ -67,7 +65,7 @@ public class XDefPsiHelper { return XDslConstants.XDSL_SCHEMA_XPL; } - String ns = XmlPsiHelper.getXmlnsForUrl(tag, XDslConstants.XDSL_SCHEMA_XDEF); + String ns = XmlPsiHelper.getXmlnsForUrl(tag, XDslConstants.XDSL_SCHEMA_XDSL); String key; if (ns == null) { key = XDslKeys.DEFAULT.SCHEMA; @@ -101,36 +99,41 @@ public class XDefPsiHelper { public static XmlTagInfo getTagInfo(String schemaUrl, XmlTag tag) { IXDefinition def = loadSchema(schemaUrl); - if (def == null) + if (def == null) { return null; + } IXDefNode dslDefNode = getXDslDef().getRootNode(); + // 通过任意未定义的子节点名称,得到 xpl 的 xdef:unknown-tag 子节点定义 IXDefNode xplDefNode = getXplDef().getRootNode().getChild("div"); List tags = getSelfAndParents(tag); - XmlTagInfo tagInfo = null; tags = CollectionHelper.reverseList(tags); boolean xpl = false; + XmlTagInfo tagInfo = null; for (int i = 0, n = tags.size(); i < n; i++) { XmlTag xmlTag = tags.get(i); + if (i == 0) { - tagInfo = new XmlTagInfo(xmlTag, def.getRootNode(), null, false, - dslDefNode, def); + tagInfo = new XmlTagInfo(xmlTag, def.getRootNode(), null, false, dslDefNode, def); } else { XmlTagInfo parent = tagInfo; - String tagName = xmlTag.getName(); - if (tagName.startsWith("xdsl:")) - tagName = "x:" + tagName.substring("xdsl:".length()); + String tagName = normalizeName(xmlTag.getName()); dslDefNode = parent.getDslNodeChild(tagName); - IXDefNode defNode = tagName.startsWith("x:") ? dslDefNode : parent.getDefNodeChild(tagName); + IXDefNode defNode = tagName.startsWith("x:") ? dslDefNode : parent.getDefNodeChild(tagName); if (defNode == null) { defNode = xpl ? xplDefNode : null; } - tagInfo = new XmlTagInfo(xmlTag, defNode, - parent.getDefNode(), parent.isCustom() || parent.isSupportBody(), dslDefNode, def); + + tagInfo = new XmlTagInfo(xmlTag, + defNode, + parent.getDefNode(), + parent.isCustom() || parent.isSupportBody(), + dslDefNode, + def); if (isXplNode(defNode)) { xpl = true; @@ -140,21 +143,34 @@ public class XDefPsiHelper { return tagInfo; } + public static String normalizeName(String name) { + if (name.startsWith("xdsl:")) { + name = "x:" + name.substring("xdsl:".length()); + } else if (name.startsWith("meta:")) { + name = "xdef:" + name.substring("meta:".length()); + } + return name; + } + static boolean isXplNode(IXDefNode defNode) { - if (defNode == null) + if (defNode == null) { return false; - if (defNode.getXdefValue() == null) + } + if (defNode.getXdefValue() == null) { return false; + } String stdDomain = defNode.getXdefValue().getStdDomain(); return stdDomain.equals("xpl") || stdDomain.startsWith("xpl-"); } + /** 自底向上查找 tag 所在分支上的节点 */ static List getSelfAndParents(XmlTag tag) { List ret = new ArrayList<>(); + while (tag != null) { ret.add(tag); tag = tag.getParentTag(); } return ret; } -} \ No newline at end of file +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java index 29a0ad26f..d73d216ad 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java @@ -11,6 +11,7 @@ import com.intellij.psi.xml.XmlTag; import io.nop.commons.util.StringHelper; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefinition; +import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.domain.StdDomainRegistry; public class XmlTagInfo { @@ -21,8 +22,9 @@ public class XmlTagInfo { private final IXDefNode dslNode; private final IXDefinition xdef; - public XmlTagInfo(XmlTag tag, IXDefNode defNode, IXDefNode parentDefNode, - boolean custom, IXDefNode dslNode, IXDefinition xdef) { + public XmlTagInfo( + XmlTag tag, IXDefNode defNode, IXDefNode parentDefNode, boolean custom, IXDefNode dslNode, IXDefinition xdef + ) { this.tag = tag; this.defNode = defNode; this.parentDefNode = parentDefNode; @@ -32,12 +34,14 @@ public class XmlTagInfo { } public boolean isAllowedUnknownName(String name) { - if (!StringHelper.hasNamespace(name)) + if (!StringHelper.hasNamespace(name)) { return false; + } String ns = StringHelper.getNamespace(name); - if (xdef.getXdefCheckNs() == null || xdef.getXdefCheckNs().isEmpty()) + if (xdef.getXdefCheckNs() == null || xdef.getXdefCheckNs().isEmpty()) { return true; + } return !xdef.getXdefCheckNs().contains(ns); } @@ -55,14 +59,16 @@ public class XmlTagInfo { } public IXDefNode getDslNodeChild(String tagName) { - if (dslNode == null) + if (dslNode == null) { return null; + } return dslNode.getChild(tagName); } public IXDefNode getDefNodeChild(String tagName) { - if (defNode == null) + if (defNode == null) { return null; + } return defNode.getChild(tagName); } @@ -71,10 +77,12 @@ public class XmlTagInfo { } public boolean isSupportBody() { - if (defNode == null) + if (defNode == null) { return false; - if (defNode.getXdefValue() == null) + } + if (defNode.getXdefValue() == null) { return false; + } return defNode.getXdefValue().isSupportBody(StdDomainRegistry.instance()); } @@ -85,4 +93,18 @@ public class XmlTagInfo { public IXDefNode getDefNode() { return defNode; } + + public XDefTypeDecl getAttrType(String attrName) { + attrName = XDefPsiHelper.normalizeName(attrName); + + XDefTypeDecl defType = null; + if (attrName.startsWith("x:")) { + defType = getDslNode().getAttrType(attrName); + } + + if (defType == null) { + defType = getDefNode().getAttrType(attrName); + } + return defType; + } } diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 317738858..31e4116c1 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -85,7 +85,7 @@ - + diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index d44e0a3bc..ce66b3e53 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -2,7 +2,7 @@ package io.nop.idea.plugin; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.FileTypeManager; -import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; import io.nop.api.core.ApiConfigs; import io.nop.api.core.config.AppConfig; import io.nop.commons.lang.impl.Cancellable; @@ -10,6 +10,9 @@ import io.nop.core.dict.DictProvider; import io.nop.core.initialize.ICoreInitializer; import io.nop.core.initialize.impl.ReflectionHelperMethodInitializer; import io.nop.core.initialize.impl.VirtualFileSystemInitializer; +import io.nop.core.resource.IResource; +import io.nop.core.resource.ResourceHelper; +import io.nop.core.resource.VirtualFileSystem; import io.nop.idea.plugin.lang.XLangFileType; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.xlang.initialize.XLangCoreInitializer; @@ -18,7 +21,7 @@ import io.nop.xlang.initialize.XLangCoreInitializer; * @author flytreeleft * @date 2025-06-17 */ -public abstract class BaseXLangPluginTestCase extends BasePlatformTestCase { +public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtureTestCase { private final Cancellable cleanup = new Cancellable(); @Override @@ -63,4 +66,18 @@ public abstract class BaseXLangPluginTestCase extends BasePlatformTestCase { protected String[] getXLangFileExtensions() { return new String[0]; } + + /** 将测试环境中的 vfs 资源添加到 Project 中 */ + protected void addVfsResourcesToProject(String... resources) { + for (String resource : resources) { + String text = readVfsResource(resource); + + myFixture.addFileToProject("_vfs" + resource, text); + } + } + + protected String readVfsResource(String resource) { + IResource res = VirtualFileSystem.instance().getResource(resource); + return ResourceHelper.readText(res); + } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java new file mode 100644 index 000000000..5c4b876e3 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -0,0 +1,139 @@ +package io.nop.idea.plugin.link; + +import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlFile; +import io.nop.idea.plugin.BaseXLangPluginTestCase; + +/** + * 参考 https://github.com/JetBrains/intellij-community/blob/master/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyGoToTypeDeclarationTest.java + * + * @author flytreeleft + * @date 2025-06-17 + */ +public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { + private static final String XLANG_EXT = "xlang"; + + @Override + protected String[] getXLangFileExtensions() { + return new String[] { XLANG_EXT }; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + addVfsResourcesToProject("/nop/schema/xdef.xdef", + "/nop/schema/xdsl.xdef", + "/nop/schema/xmeta.xdef", + "/nop/schema/xui/xview.xdef", + "/nop/schema/xui/store.xdef", + "/nop/core/xlib/meta-gen.xlib", + "/test/link/a.xmeta", + "/test/link/b.xmeta", + "/test/link/default.xform"); + } + + public void testGetGotoDeclarationTargetsForXmlTag() { + } + + public void testGetGotoDeclarationTargetsForXmlAttributeValue() { + // 根据在 schema 中定义的属性类型决定跳转 + // - x:schema=v-path + doTest(""" + + """, "/nop/schema/xmeta.xdef"); + // - x:extends=v-path-list + doTest(""" + + """, "/test/link/a.xmeta"); + doTest(""" + + """, "/test/link/b.xmeta"); + // - xdef:default-extends=v-path + doTest(""" +
+ """, "/test/link/default.xform"); + // - xpl:lib=v-path + doTest(""" + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); + + // 对 xdef.xdef 中引用的跳转 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", + "x:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + + // 对 xdsl.xdef 中引用的跳转 + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", + "xdsl:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + + // 未定义类型的属性,直接对 *.xdef 做跳转 + doTest(""" + + """, "/nop/schema/xdsl.xdef"); + // - 选中光标左侧文件 + doTest(""" + + """, "/nop/schema/xui/xview.xdef"); + // - 选中光标右侧文件 + doTest(""" + + """, "/nop/schema/xui/store.xdef"); + + // 对 xdef-ref 类型属性的跳转 +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", +// "meta:ref=\"XDefNode\""), +// "/nop/schema/xdef.xdef"); +// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref="DslNode"", +// "xdef:ref="DslNode""), +// "/nop/schema/xdef.xdef"); + } + + public void testGetGotoDeclarationTargetsForXmlText() { + } + + /** 通过在 text 中插入 <caret> 代表光标位置 */ + private void doTest(String text, String... expected) { + myFixture.configureByText("example." + XLANG_EXT, text); + + PsiElement[] refs = GotoDeclarationAction.findAllTargetElements(getProject(), + myFixture.getEditor(), + myFixture.getCaretOffset()); + assertNotNull(refs); + assertEquals(refs.length, expected.length); + + for (int i = 0; i < refs.length; i++) { + PsiElement ref = refs[i]; + String exp = expected[i]; + + if (ref instanceof XmlFile) { + String file = ((XmlFile) ref).getVirtualFile().toString(); + String actual = file.substring(file.indexOf("/_vfs/") + "/_vfs".length()); + + assertEquals(exp, actual); + } + } + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestXDefPsiHelper.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestXDefPsiHelper.java new file mode 100644 index 000000000..a9bfe82e8 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestXDefPsiHelper.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.utils; + +import io.nop.idea.plugin.BaseXLangPluginTestCase; +import io.nop.xlang.xdef.IXDefinition; + +/** + * @author flytreeleft + * @date 2025-06-18 + */ +public class TestXDefPsiHelper extends BaseXLangPluginTestCase { + + public void testGetXdefDef() { + IXDefinition xdef = XDefPsiHelper.getXdefDef(); + + assertNotNull(xdef); + } +} diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xmeta new file mode 100644 index 000000000..928def06d --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xmeta @@ -0,0 +1,2 @@ + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/b.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/link/b.xmeta new file mode 100644 index 000000000..928def06d --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/link/b.xmeta @@ -0,0 +1,2 @@ + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/default.xform b/nop-idea-plugin/src/test/resources/_vfs/test/link/default.xform new file mode 100644 index 000000000..76dac931f --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/link/default.xform @@ -0,0 +1,2 @@ + -- Gitee From 11ff5b382e158c10a29ea16d6655b02606a882df Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 19 Jun 2025 13:53:02 +0800 Subject: [PATCH 04/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20xdef-ref=20=E7=B1=BB=E5=9E=8B=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E7=9A=84=20vfs=20=E6=96=87=E4=BB=B6=E5=BC=95=E7=94=A8=E5=92=8C?= =?UTF-8?q?=20xdef:name=20=E5=90=8D=E5=AD=97=E5=BC=95=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 6 +- .../XLangCompletionContributor.java | 4 +- .../link/XLangGotoDeclarationHandler.java | 104 ++++++++++-------- .../nop/idea/plugin/utils/XDefPsiHelper.java | 59 ++++++---- .../io/nop/idea/plugin/utils/XmlTagInfo.java | 75 ++++++++----- .../link/TestXLangGotoDeclarationHandler.java | 42 +++++-- .../resources/_vfs/test/link/test-filter.xdef | 6 + 7 files changed, 183 insertions(+), 113 deletions(-) create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/link/test-filter.xdef diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index 66f32600c..7d6bcff7a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -153,8 +153,8 @@ public class XLangAnnotator implements Annotator { IXDefAttribute defAttr = null; // 识别系统保留名字空间 if (attrName.startsWith("x:")) { - if (tagInfo.getDslNode() != null) { - defAttr = tagInfo.getDslNode().getAttribute(attrName); + if (tagInfo.getXDslDefNode() != null) { + defAttr = tagInfo.getXDslDefNode().getAttribute(attrName); if (defAttr != null) attrType = defAttr.getType(); } @@ -285,4 +285,4 @@ public class XLangAnnotator implements Annotator { } return element; } -} \ No newline at end of file +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java index 551393bc0..9f080bb46 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java @@ -130,7 +130,7 @@ public class XLangCompletionContributor extends CompletionContributor implements String prefix = result.getPrefixMatcher().getPrefix(); IXDefNode defNode = tagInfo.getDefNode(); if (prefix.startsWith("x:")) { - defNode = tagInfo.getDslNode(); + defNode = tagInfo.getXDslDefNode(); } if (defNode != null) { for (IXDefAttribute defAttr : defNode.getAttributes().values()) { @@ -221,4 +221,4 @@ public class XLangCompletionContributor extends CompletionContributor implements label = null; return LookupElementBuilder.create(value).withTailText(label); } -} \ No newline at end of file +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java index 4d9d3dd61..073f4b1d4 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -7,6 +7,9 @@ */ package io.nop.idea.plugin.link; +import java.util.ArrayList; +import java.util.List; + import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandlerBase; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; @@ -82,16 +85,17 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return null; } + String attrName = attr.getName(); XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); XDefTypeDecl attrDefType = null; if (tagInfo != null && tagInfo.getDefNode() != null) { - attrDefType = tagInfo.getAttrType(attr.getName()); + attrDefType = tagInfo.getAttrType(attrName); } if (attrDefType == null) { // 未定义类型属性,直接针对 *.xdef 文件做跳转 - if (attrValue.endsWith(".xdef") && attrValue.contains("/")) { + if (attrValue.endsWith(".xdef")) { return getPsiFilesFromPathCsv(project, attr.getContainingFile(), cursorOffset); } return null; @@ -103,21 +107,16 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { } // else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { return getPsiFilesFromPathCsv(project, attr.getContainingFile(), cursorOffset); + } // + else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { + return getPsiFilesByXDefRef(project, tagInfo, attrValue); } + // + // + // XDefConstants.STD_DOMAIN_METHOD_REF // XDefConstants.STD_DOMAIN_NAME_OR_V_PATH - // XDefConstants.STD_DOMAIN_XDEF_REF - -// if (attrName.equals("xpl:lib")) { -// String path = XmlPsiHelper.absolutePath(attrValue, attr); -// -// return XmlPsiHelper.findPsiFile(project, path); -// } else if (isVfsPath(attr)) { -// String path = XmlPsiHelper.absolutePath(attrValue, attr); -// -// return XmlPsiHelper.findPsiFile(project, path); -// } return null; } @@ -194,6 +193,50 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return csv.substring(start, end); } + private PsiElement[] getPsiFilesByXDefRef(Project project, XmlTagInfo tagInfo, String attrValue) { + // - /nop/schema/xdef.xdef: + // - `` + // - `` + // - /nop/schema/schema/schema-node.xdef: + // ``,`FilterCondition` 为节点的唯一属性值 + String target; + PsiElement[] psiFiles; + + if (attrValue.indexOf(".") > 0) { + int hashIndex = attrValue.indexOf('#'); + String path = hashIndex > 0 ? attrValue.substring(0, hashIndex) : attrValue; + + target = hashIndex > 0 ? attrValue.substring(hashIndex + 1) : null; + psiFiles = getPsiFilesByPath(project, tagInfo.getTag(), path); + } else { + target = attrValue; + // Note: 只能引用当前文件内的名字 + psiFiles = new PsiElement[] { tagInfo.getTag().getContainingFile() }; + } + + if (psiFiles == null || StringHelper.isEmpty(target)) { + return psiFiles; + } + + List result = new ArrayList<>(); + for (PsiElement psiFile : psiFiles) { + PsiTreeUtil.processElements(psiFile, element -> { + if (element instanceof XmlTag tag) { + if (target.equals(tag.getAttributeValue("xdef:name")) // + || target.equals(tag.getAttributeValue("meta:name")) // + || target.equals(tag.getAttributeValue("name")) // + || target.equals(tag.getAttributeValue("id")) // + ) { + result.add(tag); + } + } + return true; // 继续遍历 + }); + } + + return result.isEmpty() ? psiFiles : result.toArray(new PsiElement[0]); + } + private boolean isCustomTag(String tagName) { int pos = tagName.indexOf(':'); if (pos <= 0) { @@ -210,39 +253,4 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { && !ns.equals("macro") && !ns.equals("xmlns"); } - - private boolean isVfsPath(XmlAttribute attr) { - String value = attr.getValue(); - String name = attr.getName(); - - if (StringHelper.isEmpty(value)) { - return false; - } - - if (value.indexOf(':') >= 0) { - return false; - } - - if ("x:extends".equals(name)) { - return true; - } - - if ("x:schema".equals(name)) { - return true; - } - - if (name.startsWith("xmlns:") && value.endsWith(".xdef")) { - return true; - } - - if (name.equals("xdef:ref") || name.equals("ref")) { - if (value.indexOf('.') > 0) { - return true; - } - } - - // - // - return StringHelper.isValidFilePath(value); - } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index cf606c222..07e8ca8b5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -72,8 +72,8 @@ public class XDefPsiHelper { } else { key = ns + ":schema"; } - String schemaPath = tag.getAttributeValue(key); - return schemaPath; + + return tag.getAttributeValue(key); } public static IXDefinition loadSchema(String schemaUrl) { @@ -103,37 +103,45 @@ public class XDefPsiHelper { return null; } - IXDefNode dslDefNode = getXDslDef().getRootNode(); + IXDefNode xdslDefNode = getXDslDef().getRootNode(); // 通过任意未定义的子节点名称,得到 xpl 的 xdef:unknown-tag 子节点定义 - IXDefNode xplDefNode = getXplDef().getRootNode().getChild("div"); + IXDefNode xplDefNode = getXplDef().getRootNode().getChild("any"); List tags = getSelfAndParents(tag); tags = CollectionHelper.reverseList(tags); + XmlTag rootTag = tags.get(0); + String xdefNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDEF); + String xdslNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); + boolean xpl = false; XmlTagInfo tagInfo = null; for (int i = 0, n = tags.size(); i < n; i++) { XmlTag xmlTag = tags.get(i); if (i == 0) { - tagInfo = new XmlTagInfo(xmlTag, def.getRootNode(), null, false, dslDefNode, def); + tagInfo = new XmlTagInfo(xmlTag, def, def.getRootNode(), null, // + xdslDefNode, false, xdefNs, xdslNs); } else { - XmlTagInfo parent = tagInfo; - String tagName = normalizeName(xmlTag.getName()); + XmlTagInfo parentTagInfo = tagInfo; + // Note: 对 xpl 节点的名字空间不做转换,也就是不限定其名字空间 + String tagName = xpl ? xmlTag.getName() : normalizeNamespace(xmlTag.getName(), xdefNs, xdslNs); - dslDefNode = parent.getDslNodeChild(tagName); + xdslDefNode = parentTagInfo.getXDslDefNodeChild(tagName); - IXDefNode defNode = tagName.startsWith("x:") ? dslDefNode : parent.getDefNodeChild(tagName); + IXDefNode defNode = tagName.startsWith("x:") ? xdslDefNode : parentTagInfo.getDefNodeChild(tagName); if (defNode == null) { defNode = xpl ? xplDefNode : null; } tagInfo = new XmlTagInfo(xmlTag, + def, defNode, - parent.getDefNode(), - parent.isCustom() || parent.isSupportBody(), - dslDefNode, - def); + parentTagInfo.getDefNode(), + xdslDefNode, + parentTagInfo.isCustom() || parentTagInfo.isSupportBody(), + xdefNs, + xdslNs); if (isXplNode(defNode)) { xpl = true; @@ -143,22 +151,27 @@ public class XDefPsiHelper { return tagInfo; } - public static String normalizeName(String name) { - if (name.startsWith("xdsl:")) { - name = "x:" + name.substring("xdsl:".length()); - } else if (name.startsWith("meta:")) { - name = "xdef:" + name.substring("meta:".length()); + /** + * 对 xml 的标签和属性名中的名字空间做转换, + * 从而支持对 xdsl.xdef 和 xdef.xdef 中的节点和属性的文档显示和引用跳转 + */ + public static String normalizeNamespace(String xmlName, String xdefNs, String xdslNs) { + // 转换 /nop/schema/xdsl.xdef 的 xdsl 名字空间 + if (xdslNs != null && !"x".equals(xdslNs) && xmlName.startsWith(xdslNs + ":")) { + xmlName = "x:" + xmlName.substring((xdslNs + ":").length()); + } + // 转换 /nop/schema/xdef.xdef 的 meta 名字空间 + else if (xdefNs != null && !"xdef".equals(xdefNs) && xmlName.startsWith(xdefNs + ":")) { + xmlName = "xdef:" + xmlName.substring((xdefNs + ":").length()); } - return name; + return xmlName; } static boolean isXplNode(IXDefNode defNode) { - if (defNode == null) { - return false; - } - if (defNode.getXdefValue() == null) { + if (defNode == null || defNode.getXdefValue() == null) { return false; } + String stdDomain = defNode.getXdefValue().getStdDomain(); return stdDomain.equals("xpl") || stdDomain.startsWith("xpl-"); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java index d73d216ad..d62aec229 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java @@ -9,6 +9,7 @@ package io.nop.idea.plugin.utils; import com.intellij.psi.xml.XmlTag; import io.nop.commons.util.StringHelper; +import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefinition; import io.nop.xlang.xdef.XDefTypeDecl; @@ -16,21 +17,30 @@ import io.nop.xlang.xdef.domain.StdDomainRegistry; public class XmlTagInfo { private final XmlTag tag; + + private final IXDefinition def; private final IXDefNode defNode; private final IXDefNode parentDefNode; + private final IXDefNode xdslDefNode; + private final boolean custom; - private final IXDefNode dslNode; - private final IXDefinition xdef; + private final String xdefNs; + private final String xdslNs; public XmlTagInfo( - XmlTag tag, IXDefNode defNode, IXDefNode parentDefNode, boolean custom, IXDefNode dslNode, IXDefinition xdef + XmlTag tag, // + IXDefinition def, IXDefNode defNode, IXDefNode parentDefNode, // + IXDefNode xdslDefNode, // + boolean custom, String xdefNs, String xdslNs ) { this.tag = tag; + this.def = def; this.defNode = defNode; this.parentDefNode = parentDefNode; + this.xdslDefNode = xdslDefNode; this.custom = custom; - this.dslNode = dslNode; - this.xdef = xdef; + this.xdefNs = xdefNs; + this.xdslNs = xdslNs; } public boolean isAllowedUnknownName(String name) { @@ -39,37 +49,41 @@ public class XmlTagInfo { } String ns = StringHelper.getNamespace(name); - if (xdef.getXdefCheckNs() == null || xdef.getXdefCheckNs().isEmpty()) { + if (def.getXdefCheckNs() == null || def.getXdefCheckNs().isEmpty()) { return true; } - return !xdef.getXdefCheckNs().contains(ns); + return !def.getXdefCheckNs().contains(ns); + } + + public IXDefinition getDef() { + return def; } - public IXDefinition getXdef() { - return xdef; + public IXDefNode getDefNode() { + return defNode; } - public IXDefNode getDslNode() { - return dslNode; + public IXDefNode getDefNodeChild(String tagName) { + if (defNode == null) { + return null; + } + return defNode.getChild(tagName); } public IXDefNode getParentDefNode() { return parentDefNode; } - public IXDefNode getDslNodeChild(String tagName) { - if (dslNode == null) { - return null; - } - return dslNode.getChild(tagName); + public IXDefNode getXDslDefNode() { + return xdslDefNode; } - public IXDefNode getDefNodeChild(String tagName) { - if (defNode == null) { + public IXDefNode getXDslDefNodeChild(String tagName) { + if (xdslDefNode == null) { return null; } - return defNode.getChild(tagName); + return xdslDefNode.getChild(tagName); } public boolean isCustom() { @@ -90,21 +104,22 @@ public class XmlTagInfo { return tag; } - public IXDefNode getDefNode() { - return defNode; + public IXDefAttribute getAttr(String attrName) { + attrName = XDefPsiHelper.normalizeNamespace(attrName, xdefNs, xdslNs); + + IXDefAttribute attr = getDefNode().getAttribute(attrName); + if (attr == null) { + attr = getXDslDefNode().getAttribute(attrName); + } + return attr; } public XDefTypeDecl getAttrType(String attrName) { - attrName = XDefPsiHelper.normalizeName(attrName); - - XDefTypeDecl defType = null; - if (attrName.startsWith("x:")) { - defType = getDslNode().getAttrType(attrName); - } + IXDefAttribute attr = getAttr(attrName); - if (defType == null) { - defType = getDefNode().getAttrType(attrName); + if (attr == null) { + return getDefNode().getXdefUnknownAttr(); } - return defType; + return attr.getType(); } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java index 5c4b876e3..3c12fd416 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -3,6 +3,7 @@ package io.nop.idea.plugin.link; import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; import com.intellij.psi.PsiElement; import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; import io.nop.idea.plugin.BaseXLangPluginTestCase; /** @@ -29,9 +30,11 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { "/nop/schema/xui/xview.xdef", "/nop/schema/xui/store.xdef", "/nop/core/xlib/meta-gen.xlib", + "/nop/schema/schema/obj-schema.xdef", "/test/link/a.xmeta", "/test/link/b.xmeta", - "/test/link/default.xform"); + "/test/link/default.xform", + "/test/link/test-filter.xdef"); } public void testGetGotoDeclarationTargetsForXmlTag() { @@ -103,12 +106,25 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { """, "/nop/schema/xui/store.xdef"); // 对 xdef-ref 类型属性的跳转 -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", -// "meta:ref=\"XDefNode\""), -// "/nop/schema/xdef.xdef"); -// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref="DslNode"", -// "xdef:ref="DslNode""), -// "/nop/schema/xdef.xdef"); + // - 在 *.xdef 中引用内部名字 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", + "meta:ref=\"XDefNode\""), "XDefNode"); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), + "DslNode"); + // - 在 *.xdef 中引用外部文件 + doTest(""" + + """, "/nop/schema/schema/obj-schema.xdef"); + // - 在 *.xmeta 中引用外部文件中的节点 + doTest(""" + + """, "FilterCondition"); } public void testGetGotoDeclarationTargetsForXmlText() { @@ -128,10 +144,22 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { PsiElement ref = refs[i]; String exp = expected[i]; + // 引用文件 if (ref instanceof XmlFile) { String file = ((XmlFile) ref).getVirtualFile().toString(); String actual = file.substring(file.indexOf("/_vfs/") + "/_vfs".length()); + assertEquals(exp, actual); + } + // 引用节点 + else if (ref instanceof XmlTag tag) { + // Note: xdef.xdef 中的 meta:name 才是节点名 + String actual = tag.getAttributeValue("meta:name"); + if (actual == null) { + // 其他 *.xdef 中的节点名为 xdef:name + actual = tag.getAttributeValue("xdef:name"); + } + assertEquals(exp, actual); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/test-filter.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/link/test-filter.xdef new file mode 100644 index 000000000..4cc333d4d --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/link/test-filter.xdef @@ -0,0 +1,6 @@ + + + -- Gitee From 6fcd511d2ac62fcede386cb74e701e3145ca8da2 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 19 Jun 2025 18:13:59 +0800 Subject: [PATCH 05/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20x:prototype=20=E5=90=8D=E5=AD=97=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E8=B7=B3=E8=BD=AC=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../link/XLangGotoDeclarationHandler.java | 150 +++++++++++------- .../nop/idea/plugin/utils/XDefPsiHelper.java | 26 +-- .../nop/idea/plugin/utils/XmlPsiHelper.java | 23 +++ .../link/TestXLangGotoDeclarationHandler.java | 150 ++++++++++-------- .../src/test/resources/_vfs/test/link/a.xlib | 3 + .../resources/_vfs/test/link/user.view.xml | 18 +++ 6 files changed, 233 insertions(+), 137 deletions(-) create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/link/user.view.xml diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java index 073f4b1d4..08564dd2b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -24,13 +24,14 @@ import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.utils.XmlTagInfo; +import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /*** - * 点击ctrl能够链接到lib文件 + * 点击 + Ctrl 能够跳转到引用元素 */ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { @@ -60,7 +61,7 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return null; } - /** 获取可从 xml 标签上跳转的元素(文件路径、节点引用等) */ + /** 获取可从 xml 标签上跳转的元素(文件路径、节点引用、xpl 函数等) */ private PsiElement[] getGotoDeclarationTargetsForXmlTag(Project project, PsiElement element) { XmlTag tag = (XmlTag) element; String tagName = tag.getName(); @@ -93,32 +94,33 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { attrDefType = tagInfo.getAttrType(attrName); } - if (attrDefType == null) { - // 未定义类型属性,直接针对 *.xdef 文件做跳转 - if (attrValue.endsWith(".xdef")) { - return getPsiFilesFromPathCsv(project, attr.getContainingFile(), cursorOffset); + PsiFile file = attr.getContainingFile(); + if (attrDefType != null) { + String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); + String stdDomain = attrDefType.getStdDomain(); + + if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain)) { + return getGotoDeclarationTargetsByPath(project, attr, attrValue); + } // + else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { + return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); + } // + else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { + return getGotoDeclarationTargetsFromXDefRef(project, attr, attrValue); + } // + else if ((xdslNs + ":prototype").equals(attrName)) { + return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); } - return null; - } - - String stdDomain = attrDefType.getStdDomain(); - if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain)) { - return getPsiFilesByPath(project, attr, attrValue); - } // - else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { - return getPsiFilesFromPathCsv(project, attr.getContainingFile(), cursorOffset); - } // - else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { - return getPsiFilesByXDefRef(project, tagInfo, attrValue); } + // 其他有效文件均可跳转 // // - - // XDefConstants.STD_DOMAIN_METHOD_REF - // XDefConstants.STD_DOMAIN_NAME_OR_V_PATH - - return null; + // + if (attrValue.indexOf(',') > 0) { + return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); + } + return getGotoDeclarationTargetsByPath(project, attr, attrValue); } /** 获取可从 xml 文本中跳转的元素(文件路径、节点引用等) */ @@ -139,8 +141,8 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return null; } - /** 获取指定路径下的文件 */ - private PsiElement[] getPsiFilesByPath(Project project, @NotNull XmlElement element, String path) { + /** 获取指定路径的跳转元素(文件) */ + private PsiElement[] getGotoDeclarationTargetsByPath(Project project, @NotNull XmlElement element, String path) { if (!StringHelper.isValidFilePath(path)) { return null; } @@ -150,8 +152,10 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return XmlPsiHelper.findPsiFile(project, path); } - /** 从 csv 文本中取光标处的文件 */ - private PsiElement[] getPsiFilesFromPathCsv(Project project, @NotNull PsiFile file, int cursorOffset) { + /** 从 csv 文本中取光标处的跳转元素(文件) */ + private PsiElement[] getGotoDeclarationTargetsFromPathCsv( + Project project, @NotNull PsiFile file, int cursorOffset + ) { PsiElement element = file.findElementAt(cursorOffset); assert element != null; @@ -165,40 +169,18 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { String path = extractPathFromCsv(element.getText(), cursorOffset - elementStart); - return getPsiFilesByPath(project, (XmlElement) element, path); - } - - /** 从 csv 中提取指定偏移位置所在的文件路径 */ - private String extractPathFromCsv(String csv, int offset) { - int start = offset; - int end = offset; - - while (start > 0) { - char ch = csv.charAt(start - 1); - if (ch != ',' && !Character.isWhitespace(ch)) { - start -= 1; - } else { - break; - } - } - while (end < csv.length()) { - char ch = csv.charAt(end); - if (ch != ',' && !Character.isWhitespace(ch)) { - end += 1; - } else { - break; - } - } - - return csv.substring(start, end); + return getGotoDeclarationTargetsByPath(project, (XmlElement) element, path); } - private PsiElement[] getPsiFilesByXDefRef(Project project, XmlTagInfo tagInfo, String attrValue) { + /** 从 xdef-ref 类型的属性值中获得跳转元素(文件或节点) */ + private PsiElement[] getGotoDeclarationTargetsFromXDefRef( + Project project, @NotNull XmlElement element, String attrValue + ) { // - /nop/schema/xdef.xdef: // - `` // - `` // - /nop/schema/schema/schema-node.xdef: - // ``,`FilterCondition` 为节点的唯一属性值 + // `` String target; PsiElement[] psiFiles; @@ -207,11 +189,11 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { String path = hashIndex > 0 ? attrValue.substring(0, hashIndex) : attrValue; target = hashIndex > 0 ? attrValue.substring(hashIndex + 1) : null; - psiFiles = getPsiFilesByPath(project, tagInfo.getTag(), path); + psiFiles = getGotoDeclarationTargetsByPath(project, element, path); } else { target = attrValue; // Note: 只能引用当前文件内的名字 - psiFiles = new PsiElement[] { tagInfo.getTag().getContainingFile() }; + psiFiles = new PsiElement[] { element.getContainingFile() }; } if (psiFiles == null || StringHelper.isEmpty(target)) { @@ -220,12 +202,11 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { List result = new ArrayList<>(); for (PsiElement psiFile : psiFiles) { - PsiTreeUtil.processElements(psiFile, element -> { - if (element instanceof XmlTag tag) { + PsiTreeUtil.processElements(psiFile, el -> { + if (el instanceof XmlTag tag) { + // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 if (target.equals(tag.getAttributeValue("xdef:name")) // || target.equals(tag.getAttributeValue("meta:name")) // - || target.equals(tag.getAttributeValue("name")) // - || target.equals(tag.getAttributeValue("id")) // ) { result.add(tag); } @@ -237,6 +218,53 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return result.isEmpty() ? psiFiles : result.toArray(new PsiElement[0]); } + /** 从 x:prototype 的属性值中获得跳转元素(节点) */ + private PsiElement[] getGotoDeclarationTargetsFromPrototype( + Project project, XmlTagInfo tagInfo, String attrValue + ) { + // 仅从父节点中取引用到的子节点 + // io.nop.xlang.delta.DeltaMerger#mergePrototype + IXDefNode defNode = tagInfo.getDefNode(); + IXDefNode parentDefNode = tagInfo.getParentDefNode(); + + String keyAttr = parentDefNode.getXdefKeyAttr(); + if (keyAttr == null) { + keyAttr = defNode.getXdefUniqueAttr(); + } + + XmlTag parentTag = tagInfo.getTag().getParentTag(); + assert parentTag != null; + + XmlTag protoTag = XmlPsiHelper.getChildTagByAttr(parentTag, keyAttr, attrValue); + + return protoTag != null ? new PsiElement[] { protoTag } : null; + } + + /** 从 csv 中提取指定偏移位置所在的文件路径 */ + private String extractPathFromCsv(String csv, int offset) { + int start = offset; + int end = offset; + + while (start > 0) { + char ch = csv.charAt(start - 1); + if (ch != ',' && !Character.isWhitespace(ch)) { + start -= 1; + } else { + break; + } + } + while (end < csv.length()) { + char ch = csv.charAt(end); + if (ch != ',' && !Character.isWhitespace(ch)) { + end += 1; + } else { + break; + } + } + + return csv.substring(start, end); + } + private boolean isCustomTag(String tagName) { int pos = tagName.indexOf(':'); if (pos <= 0) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 07e8ca8b5..72f83016b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -58,22 +58,28 @@ public class XDefPsiHelper { return xplDef; } - public static String getSchemaPath(XmlTag tag) { - PsiFile file = tag.getContainingFile(); + public static String getXDslNamespace(XmlTag tag) { + XmlTag rootTag = XmlPsiHelper.getRoot(tag); + String ns = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); + + if (ns == null) { + String prefix = XDslKeys.DEFAULT.X_NS_PREFIX; + ns = prefix.substring(0, prefix.length() - 1); + } + return ns; + } + + public static String getSchemaPath(XmlTag rootTag) { + PsiFile file = rootTag.getContainingFile(); String fileExt = StringHelper.fileExt(file.getName()); if ("xpl".equals(fileExt) || "xrun".equals(fileExt) || "xgen".equals(fileExt)) { return XDslConstants.XDSL_SCHEMA_XPL; } - String ns = XmlPsiHelper.getXmlnsForUrl(tag, XDslConstants.XDSL_SCHEMA_XDSL); - String key; - if (ns == null) { - key = XDslKeys.DEFAULT.SCHEMA; - } else { - key = ns + ":schema"; - } + String ns = getXDslNamespace(rootTag); + String key = ns + ":schema"; - return tag.getAttributeValue(key); + return rootTag.getAttributeValue(key); } public static IXDefinition loadSchema(String schemaUrl) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 5deb20725..8495e9419 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -23,6 +23,7 @@ import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTokenType; +import io.nop.api.core.ApiConstants; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.core.resource.ResourceHelper; @@ -248,6 +249,28 @@ public class XmlPsiHelper { return tagNames; } + /** + * 根据属性值获取匹配的子节点,在 attrName$type 时,匹配节点的标签名 + *

+ * 其逻辑等价于 {@link io.nop.core.lang.xml.XNode#childByAttr} + */ + public static XmlTag getChildTagByAttr(XmlTag tag, String attrName, String attrValue) { + for (PsiElement element : tag.getChildren()) { + if (!(element instanceof XmlTag child)) { + continue; + } + + if (ApiConstants.TREE_BEAN_PROP_TYPE.equals(attrName)) { + if (child.getName().equals(attrName)) { + return child; + } + } else if (attrValue.equals(child.getAttributeValue(attrName))) { + return child; + } + } + return null; + } + public static XmlTag getXmlTag(PsiElement element) { if (element == null) return null; diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java index 3c12fd416..603b14c3f 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -33,6 +33,7 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { "/nop/schema/schema/obj-schema.xdef", "/test/link/a.xmeta", "/test/link/b.xmeta", + "/test/link/a.xlib", "/test/link/default.xform", "/test/link/test-filter.xdef"); } @@ -41,57 +42,91 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { } public void testGetGotoDeclarationTargetsForXmlAttributeValue() { - // 根据在 schema 中定义的属性类型决定跳转 - // - x:schema=v-path +// // 根据在 schema 中定义的属性类型决定跳转 +// // - x:schema=v-path +// doTest(""" +// +// """, "/nop/schema/xmeta.xdef"); +// // - x:extends=v-path-list +// doTest(""" +// +// """, "/test/link/a.xmeta"); +// doTest(""" +// +// """, "/test/link/b.xmeta"); +// // - xdef:default-extends=v-path +// doTest(""" +// +// """, "/test/link/default.xform"); +// // - xpl:lib=v-path +// doTest(""" +// +// +// +// +// +// """, "/nop/core/xlib/meta-gen.xlib"); +// +// // 对 xdef.xdef 中引用的跳转 +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", +// "x:schema=\"/nop/schema/xdef.xdef\""), +// "/nop/schema/xdef.xdef"); +// +// // 对 xdsl.xdef 中引用的跳转 +// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", +// "xdsl:schema=\"/nop/schema/xdef.xdef\""), +// "/nop/schema/xdef.xdef"); +// +// // 对 xdef-ref 类型属性的跳转 +// // - 在 *.xdef 中引用内部名字 +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", +// "meta:ref=\"XDefNode\""), "XDefNode"); +// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), +// "DslNode"); +// // - 在 *.xdef 中引用外部文件 +// doTest(""" +// +// """, "/nop/schema/schema/obj-schema.xdef"); +// // - 在 *.xmeta 中引用外部文件中的节点 +// doTest(""" +// +// """, "FilterCondition"); +// +// // 对 x:prototype 属性值的跳转 +// doTest(readVfsResource("/test/link/user.view.xml").replace("x:prototype=\"list\"", +// "x:prototype=\"list\""), "list"); + + // 任意有效的文件均可跳转 doTest(""" - - """, "/nop/schema/xmeta.xdef"); - // - x:extends=v-path-list + """, "/nop/schema/xdsl.xdef"); doTest(""" - - """, "/test/link/a.xmeta"); + + """, "/test/link/a.xlib"); doTest(""" - - """, "/test/link/b.xmeta"); - // - xdef:default-extends=v-path + + """, "/test/link/a.xlib"); doTest(""" - +

""", "/test/link/default.xform"); - // - xpl:lib=v-path - doTest(""" - - - - - - """, "/nop/core/xlib/meta-gen.xlib"); - - // 对 xdef.xdef 中引用的跳转 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", - "x:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - - // 对 xdsl.xdef 中引用的跳转 - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", - "xdsl:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - - // 未定义类型的属性,直接对 *.xdef 做跳转 - doTest(""" - - """, "/nop/schema/xdsl.xdef"); // - 选中光标左侧文件 doTest(""" """, "/nop/schema/xui/store.xdef"); - - // 对 xdef-ref 类型属性的跳转 - // - 在 *.xdef 中引用内部名字 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", - "meta:ref=\"XDefNode\""), "XDefNode"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), - "DslNode"); - // - 在 *.xdef 中引用外部文件 - doTest(""" - - """, "/nop/schema/schema/obj-schema.xdef"); - // - 在 *.xmeta 中引用外部文件中的节点 - doTest(""" - - """, "FilterCondition"); } public void testGetGotoDeclarationTargetsForXmlText() { @@ -160,6 +174,10 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { actual = tag.getAttributeValue("xdef:name"); } + if (actual == null) { + actual = tag.getAttributeValue("id"); + } + assertEquals(exp, actual); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib new file mode 100644 index 000000000..4d252104e --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib @@ -0,0 +1,3 @@ + + + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/user.view.xml b/nop-idea-plugin/src/test/resources/_vfs/test/link/user.view.xml new file mode 100644 index 000000000..e6ce99bce --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/link/user.view.xml @@ -0,0 +1,18 @@ + + + + /test/link/a.xmeta + + + + + + + + + + + + + -- Gitee From 1dafb2c950576197e6135c9977555058f49e0a77 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 19 Jun 2025 21:27:24 +0800 Subject: [PATCH 06/82] =?UTF-8?q?nop-idea-pugin:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=B7=B3=E8=BD=AC=E5=87=BA=E7=8E=B0=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../link/XLangGotoDeclarationHandler.java | 49 +++--- .../link/TestXLangGotoDeclarationHandler.java | 142 +++++++++--------- .../src/test/resources/_vfs/test/link/b.xmeta | 4 +- .../src/test/resources/_vfs/test/link/c.xmeta | 3 + 4 files changed, 105 insertions(+), 93 deletions(-) create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/link/c.xmeta diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java index 08564dd2b..0e22e004e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -21,6 +21,7 @@ import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.utils.XmlTagInfo; @@ -46,19 +47,22 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { @Nullable PsiElement sourceElement, int offset, Editor editor ) { Project project = editor.getProject(); - PsiElement element = sourceElement != null ? sourceElement.getParent() : null; - - if (XmlPsiHelper.isElementType(element, XmlElementType.XML_TAG)) { - return getGotoDeclarationTargetsForXmlTag(project, element); - } // - else if (XmlPsiHelper.isElementType(element, XmlElementType.XML_ATTRIBUTE_VALUE)) { - return getGotoDeclarationTargetsForXmlAttributeValue(project, element, offset); - } // - else if (XmlPsiHelper.isElementType(element, XmlElementType.XML_TEXT)) { - return getGotoDeclarationTargetsForXmlText(project, element); - } - return null; + return ProjectEnv.withProject(project, () -> { + PsiElement element = sourceElement != null ? sourceElement.getParent() : null; + + if (XmlPsiHelper.isElementType(element, XmlElementType.XML_TAG)) { + return getGotoDeclarationTargetsForXmlTag(project, element); + } // + else if (XmlPsiHelper.isElementType(element, XmlElementType.XML_ATTRIBUTE_VALUE)) { + return getGotoDeclarationTargetsForXmlAttributeValue(project, element, offset); + } // + else if (XmlPsiHelper.isElementType(element, XmlElementType.XML_TEXT)) { + return getGotoDeclarationTargetsForXmlText(project, element); + } + + return null; + }); } /** 获取可从 xml 标签上跳转的元素(文件路径、节点引用、xpl 函数等) */ @@ -99,10 +103,13 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); String stdDomain = attrDefType.getStdDomain(); - if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain)) { - return getGotoDeclarationTargetsByPath(project, attr, attrValue); - } // - else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { + // 对自身的类型声明不做处理 + if (stdDomain.equals(attrValue)) { + return null; + } + + // Note: v-path 类型采用缺省处理 + if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); } // else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { @@ -113,13 +120,10 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { } } - // 其他有效文件均可跳转 + // 缺省:有效文件均可跳转 // // // - if (attrValue.indexOf(',') > 0) { - return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); - } return getGotoDeclarationTargetsByPath(project, attr, attrValue); } @@ -184,13 +188,16 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { String target; PsiElement[] psiFiles; + // 含有后缀的,视为文件引用 if (attrValue.indexOf(".") > 0) { int hashIndex = attrValue.indexOf('#'); String path = hashIndex > 0 ? attrValue.substring(0, hashIndex) : attrValue; target = hashIndex > 0 ? attrValue.substring(hashIndex + 1) : null; psiFiles = getGotoDeclarationTargetsByPath(project, element, path); - } else { + } + // 否则,视为名字引用 + else { target = attrValue; // Note: 只能引用当前文件内的名字 psiFiles = new PsiElement[] { element.getContainingFile() }; diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java index 603b14c3f..99c229807 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -42,77 +42,55 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { } public void testGetGotoDeclarationTargetsForXmlAttributeValue() { -// // 根据在 schema 中定义的属性类型决定跳转 -// // - x:schema=v-path -// doTest(""" -// -// """, "/nop/schema/xmeta.xdef"); -// // - x:extends=v-path-list -// doTest(""" -// -// """, "/test/link/a.xmeta"); -// doTest(""" -// -// """, "/test/link/b.xmeta"); -// // - xdef:default-extends=v-path -// doTest(""" -// -// """, "/test/link/default.xform"); -// // - xpl:lib=v-path -// doTest(""" -// -// -// -// -// -// """, "/nop/core/xlib/meta-gen.xlib"); -// -// // 对 xdef.xdef 中引用的跳转 -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", -// "x:schema=\"/nop/schema/xdef.xdef\""), -// "/nop/schema/xdef.xdef"); -// -// // 对 xdsl.xdef 中引用的跳转 -// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", -// "xdsl:schema=\"/nop/schema/xdef.xdef\""), -// "/nop/schema/xdef.xdef"); -// -// // 对 xdef-ref 类型属性的跳转 -// // - 在 *.xdef 中引用内部名字 -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", -// "meta:ref=\"XDefNode\""), "XDefNode"); -// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), -// "DslNode"); -// // - 在 *.xdef 中引用外部文件 -// doTest(""" -// -// """, "/nop/schema/schema/obj-schema.xdef"); -// // - 在 *.xmeta 中引用外部文件中的节点 -// doTest(""" -// -// """, "FilterCondition"); -// -// // 对 x:prototype 属性值的跳转 -// doTest(readVfsResource("/test/link/user.view.xml").replace("x:prototype=\"list\"", -// "x:prototype=\"list\""), "list"); - - // 任意有效的文件均可跳转 + // 根据在 schema 中定义的属性类型决定跳转 + // - x:extends=v-path-list + doTest(""" + + """, "/test/link/a.xmeta"); + doTest(""" + + """, "/test/link/b.xmeta"); + + // 对 xdef.xdef 中引用的跳转 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", + "x:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + + // 对 xdsl.xdef 中引用的跳转 + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", + "xdsl:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + + // 对 xdef-ref 类型属性的跳转 + // - 在 *.xdef 中引用内部名字 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", + "meta:ref=\"XDefNode\""), "XDefNode"); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), + "DslNode"); + // - 在 *.xdef 中引用外部文件 + doTest(""" + + """, "/nop/schema/schema/obj-schema.xdef"); + // - 在 *.xmeta 中引用外部文件中的节点 + doTest(""" + + """, "FilterCondition"); + + // 对 x:prototype 属性值的跳转 + doTest(readVfsResource("/test/link/user.view.xml").replace("x:prototype=\"list\"", + "x:prototype=\"list\""), "list"); + + // 缺省:任意有效的文件均可跳转 doTest(""" """, "/test/link/default.xform"); + // - x:schema=v-path + doTest(""" + + """, "/nop/schema/xmeta.xdef"); + // - xdef:default-extends=v-path + doTest(""" + + """, "/test/link/default.xform"); + // - xpl:lib=v-path + doTest(""" + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); // - 选中光标左侧文件 doTest(""" diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/c.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/link/c.xmeta new file mode 100644 index 000000000..4849b2c41 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/link/c.xmeta @@ -0,0 +1,3 @@ + -- Gitee From 1f3c37013106f4916190a531cb27901df9028b9c Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 19 Jun 2025 21:54:26 +0800 Subject: [PATCH 07/82] =?UTF-8?q?nop-idea-pugin:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=8A=82=E7=82=B9=E5=90=8D=E7=9A=84=20x:prot?= =?UTF-8?q?otype=20=E4=B8=8D=E8=83=BD=E8=B7=B3=E8=BD=AC=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nop/idea/plugin/utils/XmlPsiHelper.java | 5 ++-- .../link/TestXLangGotoDeclarationHandler.java | 24 +++++++++---------- .../src/test/resources/_vfs/test/link/a.xlib | 5 +++- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 8495e9419..ef4b0634c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -23,7 +23,6 @@ import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTokenType; -import io.nop.api.core.ApiConstants; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.core.resource.ResourceHelper; @@ -260,8 +259,8 @@ public class XmlPsiHelper { continue; } - if (ApiConstants.TREE_BEAN_PROP_TYPE.equals(attrName)) { - if (child.getName().equals(attrName)) { + if (attrName == null) { + if (child.getName().equals(attrValue)) { return child; } } else if (attrValue.equals(child.getAttributeValue(attrName))) { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java index 99c229807..41a82687c 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -54,6 +54,11 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { x:extends="/test/link/a.xmeta,/test/link/b.xmeta" /> """, "/test/link/b.xmeta"); + doTest(""" + + """, "/test/link/a.xmeta"); // 对 xdef.xdef 中引用的跳转 doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", @@ -89,6 +94,8 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { // 对 x:prototype 属性值的跳转 doTest(readVfsResource("/test/link/user.view.xml").replace("x:prototype=\"list\"", "x:prototype=\"list\""), "list"); + doTest(readVfsResource("/test/link/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), + "Get"); // 缺省:任意有效的文件均可跳转 doTest(""" @@ -127,18 +134,6 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { """, "/nop/core/xlib/meta-gen.xlib"); - // - 选中光标左侧文件 - doTest(""" - - """, "/nop/schema/xui/xview.xdef"); - // - 选中光标右侧文件 - doTest(""" - - """, "/nop/schema/xui/store.xdef"); } public void testGetGotoDeclarationTargetsForXmlText() { @@ -178,6 +173,11 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { actual = tag.getAttributeValue("id"); } + // 缺省采用节点标签名 + if (actual == null) { + actual = tag.getName(); + } + assertEquals(exp, actual); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib index 4d252104e..edf3a7784 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib @@ -1,3 +1,6 @@ - + + + + -- Gitee From 7b61dc4631ee9ef94d09c7b316843fe3f95bdb85 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 20 Jun 2025 10:58:47 +0800 Subject: [PATCH 08/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=20xdef=20=E8=87=AA=E4=B8=BE=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E4=B8=AD=E7=9A=84=E6=A0=87=E7=AD=BE=E5=92=8C=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E6=96=87=E6=A1=A3=EF=BC=8C=E5=B9=B6=E5=9C=A8=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=A0=87=E9=A2=98=E4=B8=AD=E5=A7=8B=E7=BB=88=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=85=89=E6=A0=87=E5=A4=84=E7=9A=84=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc/XLangDocumentationProvider.java | 65 ++++++++------ .../io/nop/idea/plugin/utils/XmlTagInfo.java | 88 +++++++++++++++---- .../doc/TestXLangDocumentationProvider.java | 33 +++++-- 3 files changed, 135 insertions(+), 51 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java index d4b8c23e4..11490fe46 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java @@ -27,6 +27,7 @@ import io.nop.idea.plugin.utils.XmlTagInfo; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefComment; import io.nop.xlang.xdef.IXDefNode; +import io.nop.xlang.xdef.IXDefSubComment; import io.nop.xlang.xdef.XDefTypeDecl; import jakarta.annotation.Nullable; import org.jetbrains.annotations.NotNull; @@ -78,23 +79,26 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { return null; } - if (parent instanceof XmlTag) { + if (parent instanceof XmlTag tag) { DocInfo doc = new DocInfo(tagInfo.getDefNode()); + doc.setMainTitle(tag.getName()); - IXDefComment comment = tagInfo.getDefNode().getComment(); + IXDefComment comment = tagInfo.getComment(); if (comment != null) { - doc.setTitle(comment.getMainDisplayName()); + doc.setSubTitle(comment.getMainDisplayName()); doc.setDesc(comment.getMainDescription()); } return doc.toString(); } else if (parent instanceof XmlAttribute attr) { String attrName = attr.getName(); - DocInfo doc = new DocInfo(tagInfo.getDefNode().getAttribute(attrName)); - IXDefComment comment = tagInfo.getDefNode().getComment(); + DocInfo doc = new DocInfo(tagInfo.getAttr(attrName)); + doc.setMainTitle(attrName); + + IXDefSubComment comment = tagInfo.getAttrComment(attrName); if (comment != null) { - doc.setTitle(comment.getSubDisplayName(attrName)); - doc.setDesc(comment.getSubDescription(attrName)); + doc.setSubTitle(comment.getDisplayName()); + doc.setDesc(comment.getDescription()); } return doc.toString(); } @@ -109,25 +113,25 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { return null; } + String attrName = attr.getName(); XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); - if (tagInfo == null || tagInfo.getDefNode() == null) { - return null; - } - - XDefTypeDecl defType = tagInfo.getDefNode().getAttrType(attr.getName()); - if (defType == null || defType.getOptions() == null) { + XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getAttrType(attrName) : null; + // TODO 显示类型定义文档 + if (attrDefType == null || attrDefType.getOptions() == null) { return null; } - DictBean dictBean = DictProvider.instance().getDict(null, defType.getOptions(), null, null); + DictBean dictBean = DictProvider.instance().getDict(null, attrDefType.getOptions(), null, null); DictOptionBean option = dictBean != null ? dictBean.getOptionByValue(attr.getValue()) : null; if (option == null) { return null; } DocInfo doc = new DocInfo(); + doc.setMainTitle(option.getValue().toString()); + if (!Objects.equals(option.getLabel(), option.getValue())) { - doc.setTitle(option.getLabel()); + doc.setSubTitle(option.getLabel()); } doc.setDesc(option.getDescription()); @@ -143,7 +147,8 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { } static class DocInfo { - String title; + String mainTitle; + String subTitle; String stdDomain; String desc; @@ -155,15 +160,19 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { } DocInfo(IXDefAttribute attr) { - this(attr.getType()); + this(attr != null ? attr.getType() : null); } DocInfo(XDefTypeDecl type) { this.stdDomain = type != null ? type.getStdDomain() : null; } - public void setTitle(String title) { - this.title = title; + public void setMainTitle(String mainTitle) { + this.mainTitle = mainTitle; + } + + public void setSubTitle(String subTitle) { + this.subTitle = subTitle; } public void setDesc(String desc) { @@ -174,22 +183,22 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { public String toString() { StringBuilder sb = new StringBuilder(); - if (!StringHelper.isBlank(this.title)) { - sb.append("

"); - sb.append(StringHelper.escapeXml(this.title)); - sb.append("

"); + sb.append("

"); + sb.append(StringHelper.escapeXml(this.mainTitle)); + if (StringHelper.isNotBlank(this.subTitle)) { + sb.append(" - ").append(StringHelper.escapeXml(this.subTitle)); } + sb.append("

"); + if (this.stdDomain != null) { sb.append("

"); - sb.append("stdDomain=").append(StringHelper.escapeXml(this.stdDomain)); + sb.append("stdDomain: "); + sb.append("").append(StringHelper.escapeXml(this.stdDomain)).append(""); sb.append("

"); } if (!StringHelper.isBlank(this.desc)) { - if (!sb.isEmpty()) { - sb.append("

"); - } - + sb.append("

"); sb.append(markdown(this.desc)); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java index d62aec229..b48ba219c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java @@ -10,7 +10,9 @@ package io.nop.idea.plugin.utils; import com.intellij.psi.xml.XmlTag; import io.nop.commons.util.StringHelper; import io.nop.xlang.xdef.IXDefAttribute; +import io.nop.xlang.xdef.IXDefComment; import io.nop.xlang.xdef.IXDefNode; +import io.nop.xlang.xdef.IXDefSubComment; import io.nop.xlang.xdef.IXDefinition; import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.domain.StdDomainRegistry; @@ -43,27 +45,22 @@ public class XmlTagInfo { this.xdslNs = xdslNs; } - public boolean isAllowedUnknownName(String name) { - if (!StringHelper.hasNamespace(name)) { - return false; - } - - String ns = StringHelper.getNamespace(name); - if (def.getXdefCheckNs() == null || def.getXdefCheckNs().isEmpty()) { - return true; - } - - return !def.getXdefCheckNs().contains(ns); + /** 获取当前节点的 xml 标签 */ + public XmlTag getTag() { + return tag; } + /** 获取当前节点所在 DSL 的 xdef 定义 */ public IXDefinition getDef() { return def; } + /** 获取当前节点的 xdef 定义 */ public IXDefNode getDefNode() { return defNode; } + /** 获取当前节点指定子节点的 xdef 定义 */ public IXDefNode getDefNodeChild(String tagName) { if (defNode == null) { return null; @@ -71,14 +68,17 @@ public class XmlTagInfo { return defNode.getChild(tagName); } + /** 获取当前节点父节点的 xdef 定义 */ public IXDefNode getParentDefNode() { return parentDefNode; } + /** 获取当前节点在 xdsl.xdef 中所对应节点的 xdef 定义 */ public IXDefNode getXDslDefNode() { return xdslDefNode; } + /** 获取当前节点在 xdsl.xdef 中所对应节点的指定子节点的 xdef 定义 */ public IXDefNode getXDslDefNodeChild(String tagName) { if (xdslDefNode == null) { return null; @@ -100,26 +100,82 @@ public class XmlTagInfo { return defNode.getXdefValue().isSupportBody(StdDomainRegistry.instance()); } - public XmlTag getTag() { - return tag; + public boolean isAllowedUnknownName(String name) { + if (!StringHelper.hasNamespace(name)) { + return false; + } + + String ns = StringHelper.getNamespace(name); + if (def.getXdefCheckNs() == null || def.getXdefCheckNs().isEmpty()) { + return true; + } + + return !def.getXdefCheckNs().contains(ns); + } + + /** + * 判断当前节点上的指定属性是否为 *.xdef 的声明属性 + *

+ * 也就是,在 *.xdef 中是在定义该属性及其类型,而不是为该属性赋值。 + * 在 xdef.xdef 这类自举定义的 xdsl 中,会通过不同的名字空间来区分属性声明和属性赋值。 + * 比如,名字空间为 meta 和 x 的属性则为赋值属性,其余(无名字空间和 xdef 名字空间)的则为声明属性 + */ + public boolean isXDefDeclaredAttr(String attrName) { + String ns = StringHelper.getNamespace(attrName); + + return StringHelper.isEmpty(ns) || (!ns.equals(xdefNs) && !ns.equals(xdslNs) && !ns.equals("xmlns")); } + /** + * 获取当前节点上指定属性的 xdef 定义 + *

    + *
  • xdef.xdef 节点属性上的 meta 名字空间自动转换为 xdef
  • + *
  • xdsl.xdef 节点属性上的 xdsl 名字空间自动转换为 x
  • + *
+ */ public IXDefAttribute getAttr(String attrName) { + // TODO xpl 名字空间的节点定义在 xpl.xdef 中 attrName = XDefPsiHelper.normalizeNamespace(attrName, xdefNs, xdslNs); - IXDefAttribute attr = getDefNode().getAttribute(attrName); + IXDefAttribute attr = defNode != null ? defNode.getAttribute(attrName) : null; if (attr == null) { - attr = getXDslDefNode().getAttribute(attrName); + attr = xdslDefNode != null ? xdslDefNode.getAttribute(attrName) : null; } return attr; } + /** 获取当前节点上指定属性的类型 */ public XDefTypeDecl getAttrType(String attrName) { IXDefAttribute attr = getAttr(attrName); if (attr == null) { - return getDefNode().getXdefUnknownAttr(); + return defNode != null ? defNode.getXdefUnknownAttr() : null; } return attr.getType(); } + + /** 获取节点注释 */ + public IXDefComment getComment() { + return defNode != null ? defNode.getComment() : null; + } + + /** + * 获取指定属性的注释 + *
    + *
  • xdef.xdef 节点属性上的 meta 名字空间自动转换为 xdef
  • + *
  • xdsl.xdef 节点属性上的 xdsl 名字空间自动转换为 x
  • + *
+ */ + public IXDefSubComment getAttrComment(String attrName) { + attrName = XDefPsiHelper.normalizeNamespace(attrName, xdefNs, xdslNs); + + IXDefComment comment; + if (defNode == null || defNode.getAttribute(attrName) == null) { + comment = xdslDefNode != null ? xdslDefNode.getComment() : null; + } else { + comment = getComment(); + } + + return comment != null ? comment.getSubComments().get(attrName) : null; + } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index 3575c08bb..e839f7276 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -1,5 +1,7 @@ package io.nop.idea.plugin.doc; +import java.util.function.Consumer; + import com.intellij.codeInsight.documentation.DocumentationManager; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.psi.PsiElement; @@ -20,23 +22,36 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { } public void testGenerateDocForXmlName() { + // 显示标签文档 doTest(""" ple xmlns:x="/nop/schema/xdsl.xdef" x:schema="/test/doc/example.xdef"> - """, "

This is root node

\n"); - + """, "

example



This is root node

\n"); doTest(""" ild name="Child"/> - """, "

This is child node

\n"); - + """, "

child



This is child node

\n"); + // 显示属性文档 doTest(""" me="Child"/> - """, "

stdDomain=string



This is child name

\n"); + """, "

name

stdDomain: string



This is child name

\n"); + + // 显示 xdef.xdef 中的 meta:xxx 标签和属性文档 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("nown-tag "), + (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", + "meta:ref=\"XDefNode\""), + (genDoc) -> assertTrue(genDoc.contains("
"))); + + // 显示 xdsl.xdef 中的标签和 meta:xxx 属性文档 + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("nown-tag "), + (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema", "xdsl:schema"), + (genDoc) -> assertTrue(genDoc.contains("
"))); } public void testGenerateDocForXmlAttributeValue() { @@ -47,8 +62,12 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { """, "

leaf-Leaf Node

"); } - /** 通过在 text 中插入 <caret> 代表光标位置 */ private void doTest(String text, String doc) { + doTest(text, (genDoc) -> assertEquals(doc, genDoc)); + } + + /** 通过在 text 中插入 <caret> 代表光标位置 */ + private void doTest(String text, Consumer checker) { myFixture.configureByText("example." + XLANG_EXT, text); // Note: 通过 ApplicationManager.getApplication().runReadAction(() -> {}) @@ -61,7 +80,7 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { DocumentationProvider docProvider = DocumentationManager.getProviderFromElement(originalElement); String genDoc = docProvider.generateDoc(element, originalElement); - assertEquals(doc, genDoc); + checker.accept(genDoc); } } -- Gitee From 4667470e8c000dea4037809c7101a591c1ef90d3 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 20 Jun 2025 11:03:43 +0800 Subject: [PATCH 09/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AF=B9=20xdef.xdef/x?= =?UTF-8?q?dsl.xdef=20=E4=B8=AD=E7=9A=84=E5=A3=B0=E6=98=8E=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E4=B8=8D=E5=81=9A=E6=96=87=E4=BB=B6=E5=92=8C=E5=90=8D?= =?UTF-8?q?=E5=AD=97=E5=BC=95=E7=94=A8=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../link/XLangGotoDeclarationHandler.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java index 0e22e004e..e334458b2 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -92,32 +92,31 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { String attrName = attr.getName(); XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); + XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getAttrType(attrName) : null; + // 在无节点定义时,仅做缺省处理 + if (attrDefType == null) { + return getGotoDeclarationTargetsByPath(project, attr, attrValue); + } - XDefTypeDecl attrDefType = null; - if (tagInfo != null && tagInfo.getDefNode() != null) { - attrDefType = tagInfo.getAttrType(attrName); + // TODO 对于声明属性,仅对其类型的定义(涉及枚举和字典)做跳转 + if (tagInfo.isXDefDeclaredAttr(attrName)) { + return null; } + // 根据属性声明的类型,对属性值做引用跳转处理 PsiFile file = attr.getContainingFile(); - if (attrDefType != null) { - String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); - String stdDomain = attrDefType.getStdDomain(); - - // 对自身的类型声明不做处理 - if (stdDomain.equals(attrValue)) { - return null; - } - - // Note: v-path 类型采用缺省处理 - if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { - return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); - } // - else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { - return getGotoDeclarationTargetsFromXDefRef(project, attr, attrValue); - } // - else if ((xdslNs + ":prototype").equals(attrName)) { - return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); - } + String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); + String stdDomain = attrDefType.getStdDomain(); + + // Note: v-path 类型采用缺省处理 + if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { + return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); + } // + else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { + return getGotoDeclarationTargetsFromXDefRef(project, attr, attrValue); + } // + else if ((xdslNs + ":prototype").equals(attrName)) { + return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); } // 缺省:有效文件均可跳转 -- Gitee From 9146b19bedcb7c97186db49976e08df6a202cb77 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sat, 21 Jun 2025 17:14:37 +0800 Subject: [PATCH 10/82] =?UTF-8?q?nop-idea-pugin:=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=AF=B9=20xdef.xdef=E3=80=81xdsl.xdef=20=E7=9A=84=E8=87=AA?= =?UTF-8?q?=E4=B8=BE=E5=A4=84=E7=90=86=EF=BC=8C=E7=A1=AE=E4=BF=9D=E8=83=BD?= =?UTF-8?q?=E5=A4=9F=E5=87=86=E7=A1=AE=E6=98=BE=E7=A4=BA=E5=85=B6=E8=87=AA?= =?UTF-8?q?=E8=BA=AB=E7=9A=84=E8=8A=82=E7=82=B9=E5=92=8C=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc/XLangDocumentationProvider.java | 29 ++-- .../link/XLangGotoDeclarationHandler.java | 22 ++- .../nop/idea/plugin/utils/XDefPsiHelper.java | 90 +++++++---- .../io/nop/idea/plugin/utils/XmlTagInfo.java | 151 +++++++++++++----- .../doc/TestXLangDocumentationProvider.java | 61 ++++++- .../link/TestXLangGotoDeclarationHandler.java | 35 +++- .../test/resources/_vfs/test/doc/example.xdef | 9 +- 7 files changed, 303 insertions(+), 94 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java index 11490fe46..2d0526633 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java @@ -24,7 +24,6 @@ import io.nop.idea.plugin.utils.MarkdownHelper; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.utils.XmlTagInfo; -import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefComment; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefSubComment; @@ -83,7 +82,7 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { DocInfo doc = new DocInfo(tagInfo.getDefNode()); doc.setMainTitle(tag.getName()); - IXDefComment comment = tagInfo.getComment(); + IXDefComment comment = tagInfo.getDefNodeComment(); if (comment != null) { doc.setSubTitle(comment.getMainDisplayName()); doc.setDesc(comment.getMainDescription()); @@ -92,10 +91,10 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { } else if (parent instanceof XmlAttribute attr) { String attrName = attr.getName(); - DocInfo doc = new DocInfo(tagInfo.getAttr(attrName)); + DocInfo doc = new DocInfo(tagInfo.getDefAttrType(attrName)); doc.setMainTitle(attrName); - IXDefSubComment comment = tagInfo.getAttrComment(attrName); + IXDefSubComment comment = tagInfo.getDefAttrComment(attrName); if (comment != null) { doc.setSubTitle(comment.getDisplayName()); doc.setDesc(comment.getDescription()); @@ -115,7 +114,7 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { String attrName = attr.getName(); XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); - XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getAttrType(attrName) : null; + XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getDefAttrType(attrName) : null; // TODO 显示类型定义文档 if (attrDefType == null || attrDefType.getOptions() == null) { return null; @@ -127,11 +126,14 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { return null; } + String value = option.getStringValue(); + String label = option.getLabel(); + DocInfo doc = new DocInfo(); - doc.setMainTitle(option.getValue().toString()); + doc.setMainTitle(value); - if (!Objects.equals(option.getLabel(), option.getValue())) { - doc.setSubTitle(option.getLabel()); + if (label != null && !Objects.equals(label, value)) { + doc.setSubTitle(label.startsWith(value + '-') ? label.substring(value.length() + 1) : label); } doc.setDesc(option.getDescription()); @@ -159,12 +161,13 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { this(defNode.getXdefValue()); } - DocInfo(IXDefAttribute attr) { - this(attr != null ? attr.getType() : null); - } - DocInfo(XDefTypeDecl type) { - this.stdDomain = type != null ? type.getStdDomain() : null; + if (type != null) { + this.stdDomain = type.getStdDomain(); + if (type.getOptions() != null) { + this.stdDomain += ':' + type.getOptions(); + } + } } public void setMainTitle(String mainTitle) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java index e334458b2..a5e8bf00f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -92,20 +92,19 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { String attrName = attr.getName(); XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); - XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getAttrType(attrName) : null; + XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getDefAttrType(attrName) : null; // 在无节点定义时,仅做缺省处理 if (attrDefType == null) { return getGotoDeclarationTargetsByPath(project, attr, attrValue); } // TODO 对于声明属性,仅对其类型的定义(涉及枚举和字典)做跳转 - if (tagInfo.isXDefDeclaredAttr(attrName)) { + if (tagInfo.isDefDeclaredAttr(attrName)) { return null; } - // 根据属性声明的类型,对属性值做引用跳转处理 + // 根据属性声明的类型,对属性值做文件/名字引用跳转处理 PsiFile file = attr.getContainingFile(); - String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); String stdDomain = attrDefType.getStdDomain(); // Note: v-path 类型采用缺省处理 @@ -115,8 +114,19 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { return getGotoDeclarationTargetsFromXDefRef(project, attr, attrValue); } // - else if ((xdslNs + ":prototype").equals(attrName)) { - return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); + else { + String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); + + if ((xdslNs + ":prototype").equals(attrName)) { + return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); + } else { + String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); + if ((xdefNs + ":key-attr").equals(attrName)) { + // + } else if ((xdefNs + ":unique-attr").equals(attrName)) { + // + } + } } // 缺省:有效文件均可跳转 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 72f83016b..493301583 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -18,6 +18,7 @@ import io.nop.commons.util.StringHelper; import io.nop.core.resource.impl.ClassPathResource; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefinition; +import io.nop.xlang.xdef.XDefKeys; import io.nop.xlang.xdef.parse.XDefinitionParser; import io.nop.xlang.xdsl.XDslConstants; import io.nop.xlang.xdsl.XDslKeys; @@ -34,26 +35,27 @@ public class XDefPsiHelper { private static IXDefinition xdslDef; private static IXDefinition xplDef; + private static IXDefinition loadDef(String path) { + return new XDefinitionParser().parseFromResource(new ClassPathResource("classpath:/_vfs" + path)); + } + public static synchronized IXDefinition getXdefDef() { if (xdefDef == null) { - xdefDef = new XDefinitionParser().parseFromResource(new ClassPathResource( - "classpath:/_vfs/nop/schema/xdef.xdef")); + xdefDef = loadDef(XDslConstants.XDSL_SCHEMA_XDEF); } return xdefDef; } public static synchronized IXDefinition getXDslDef() { if (xdslDef == null) { - xdslDef = new XDefinitionParser().parseFromResource(new ClassPathResource( - "classpath:/_vfs/nop/schema/xdsl.xdef")); + xdslDef = loadDef(XDslConstants.XDSL_SCHEMA_XDSL); } return xdslDef; } public static synchronized IXDefinition getXplDef() { if (xplDef == null) { - xplDef = new XDefinitionParser().parseFromResource(new ClassPathResource( - "classpath:/_vfs/nop/schema/xpl.xdef")); + xplDef = loadDef(XDslConstants.XDSL_SCHEMA_XPL); } return xplDef; } @@ -69,6 +71,16 @@ public class XDefPsiHelper { return ns; } + public static String getXDefNamespace(XmlTag tag) { + XmlTag rootTag = XmlPsiHelper.getRoot(tag); + String ns = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDEF); + + if (ns == null) { + ns = XDefKeys.DEFAULT.NS; + } + return ns; + } + public static String getSchemaPath(XmlTag rootTag) { PsiFile file = rootTag.getContainingFile(); String fileExt = StringHelper.fileExt(file.getName()); @@ -84,8 +96,7 @@ public class XDefPsiHelper { public static IXDefinition loadSchema(String schemaUrl) { try { - IXDefinition def = SchemaLoader.loadXDefinition(schemaUrl); - return def; + return SchemaLoader.loadXDefinition(schemaUrl); } catch (Exception e) { LOG.debug("nop.load-schema-fail", e); return null; @@ -94,24 +105,42 @@ public class XDefPsiHelper { public static XmlTagInfo getTagInfo(PsiElement element) { XmlTag tag = getXmlTag(element); - if (tag != null) { - String schemaUrl = XDefPsiHelper.getSchemaPath(XmlPsiHelper.getRoot(tag)); - if (schemaUrl != null) { - return XDefPsiHelper.getTagInfo(schemaUrl, tag); - } + if (tag == null) { + return null; + } + + String schemaUrl = getSchemaPath(XmlPsiHelper.getRoot(tag)); + if (schemaUrl != null) { + return getTagInfo(schemaUrl, tag); } return null; } public static XmlTagInfo getTagInfo(String schemaUrl, XmlTag tag) { + // 在对应解析 xml 标签的元模型节点时,需要注意以下几点 + // - 若根节点的 `x:schema` 为 `/nop/schema/xdef.xdef`,则其在定义某类 DSL 的元模型, + // 若引用的是其他 xdef,则其是在定义某个具体的 DSL 模型 + // - 在普通的 XDef 元模型中,必须在根节点固定定义 `xdef` 和 `x` 名字空间: + // - `xmlns:x="/nop/schema/xdsl.xdef"` + // - `xmlns:xdef="/nop/schema/xdef.xdef"` + // 其中,以 `xdef` 为名字空间的节点和属性,用于定义 DSL 的结构,而以 + // `x` 为名字空间的节点和属性,则用于定义差量规则 + // - 而在普通的 DSL 模型中,必须在根节点固定定义 `x` 名字空间: + // - `xmlns:x="/nop/schema/xdsl.xdef"` + // 其中,以 `x` 为名字空间的节点和属性,则用于定义差量规则 + // - `/nop/schema/xdef.xdef` 本身的定义是**自举**的,其描述的是所有元模型的结构。 + // 其以 `meta` 作为名字空间来定义其 DSL 结构, + // 而以 `xdef` 为名字空间的属性和节点,则为其 DSL 的**元属性**和**元节点**, + // 用于声明其 DSL 模型所包含的属性和节点类型 + // - `/nop/schema/xdsl.xdef` 自身的定义也是**自举**的,其描述的是所有 DSL 模型的结构。 + // 其以 `xdsl` 作为名字空间,对其进行差量控制(这里主要为指定其 `schema` 为 `/nop/schema/xdef.xdef`) + // - `/nop/schema/xpl.xdef` 为 Xpl 类型节点的元模型,且以 `xpl` 为其固定的名字空间 IXDefinition def = loadSchema(schemaUrl); if (def == null) { return null; } IXDefNode xdslDefNode = getXDslDef().getRootNode(); - // 通过任意未定义的子节点名称,得到 xpl 的 xdef:unknown-tag 子节点定义 - IXDefNode xplDefNode = getXplDef().getRootNode().getChild("any"); List tags = getSelfAndParents(tag); tags = CollectionHelper.reverseList(tags); @@ -121,23 +150,32 @@ public class XDefPsiHelper { String xdslNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); boolean xpl = false; + boolean dsl = !XDslConstants.XDSL_SCHEMA_XDEF.equals(schemaUrl); XmlTagInfo tagInfo = null; for (int i = 0, n = tags.size(); i < n; i++) { XmlTag xmlTag = tags.get(i); if (i == 0) { tagInfo = new XmlTagInfo(xmlTag, def, def.getRootNode(), null, // - xdslDefNode, false, xdefNs, xdslNs); + xdslDefNode, xdefNs, xdslNs, dsl, false); } else { XmlTagInfo parentTagInfo = tagInfo; - // Note: 对 xpl 节点的名字空间不做转换,也就是不限定其名字空间 - String tagName = xpl ? xmlTag.getName() : normalizeNamespace(xmlTag.getName(), xdefNs, xdslNs); + String tagName = normalizeNamespace(xmlTag.getName(), xdefNs, xdslNs); xdslDefNode = parentTagInfo.getXDslDefNodeChild(tagName); - IXDefNode defNode = tagName.startsWith("x:") ? xdslDefNode : parentTagInfo.getDefNodeChild(tagName); - if (defNode == null) { - defNode = xpl ? xplDefNode : null; + IXDefNode defNode; + // Note: 只有不在 xdsl.xdef 中,且以 x 为名字空间的节点,才使用 xdsl 节点定义, + // 否则,保持在 XDef 元模型的节点定义 + if (tagName.startsWith("x:") && "x".equals(xdslNs)) { + defNode = xdslDefNode; + } else { + defNode = parentTagInfo.getDefNodeChild(tagName); + } + + if (defNode == null && xpl) { + // 通过任意未定义的子节点名称,得到 xpl 的 xdef:unknown-tag 子节点定义 + defNode = getXplDef().getRootNode().getChild("any"); } tagInfo = new XmlTagInfo(xmlTag, @@ -145,9 +183,10 @@ public class XDefPsiHelper { defNode, parentTagInfo.getDefNode(), xdslDefNode, - parentTagInfo.isCustom() || parentTagInfo.isSupportBody(), xdefNs, - xdslNs); + xdslNs, + dsl, + parentTagInfo.isCustom() || parentTagInfo.isSupportBody()); if (isXplNode(defNode)) { xpl = true; @@ -157,10 +196,7 @@ public class XDefPsiHelper { return tagInfo; } - /** - * 对 xml 的标签和属性名中的名字空间做转换, - * 从而支持对 xdsl.xdef 和 xdef.xdef 中的节点和属性的文档显示和引用跳转 - */ + /** 确保 `xmlName` 的 `xdef.xdef`、`xdsl.xdef` 对应的名字空间始终为 `xdef` 和 `x` */ public static String normalizeNamespace(String xmlName, String xdefNs, String xdslNs) { // 转换 /nop/schema/xdsl.xdef 的 xdsl 名字空间 if (xdslNs != null && !"x".equals(xdslNs) && xmlName.startsWith(xdslNs + ":")) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java index b48ba219c..39d1f3721 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java @@ -14,8 +14,11 @@ import io.nop.xlang.xdef.IXDefComment; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefSubComment; import io.nop.xlang.xdef.IXDefinition; +import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.domain.StdDomainRegistry; +import io.nop.xlang.xdef.impl.XDefAttribute; +import io.nop.xlang.xdef.parse.XDefTypeDeclParser; public class XmlTagInfo { private final XmlTag tag; @@ -25,24 +28,30 @@ public class XmlTagInfo { private final IXDefNode parentDefNode; private final IXDefNode xdslDefNode; - private final boolean custom; private final String xdefNs; private final String xdslNs; + /** 当前节点是否为 DSL 节点 */ + private final boolean dsl; + private final boolean custom; + public XmlTagInfo( XmlTag tag, // IXDefinition def, IXDefNode defNode, IXDefNode parentDefNode, // IXDefNode xdslDefNode, // - boolean custom, String xdefNs, String xdslNs + String xdefNs, String xdslNs, // + boolean dsl, boolean custom // ) { this.tag = tag; this.def = def; this.defNode = defNode; this.parentDefNode = parentDefNode; this.xdslDefNode = xdslDefNode; - this.custom = custom; this.xdefNs = xdefNs; this.xdslNs = xdslNs; + + this.dsl = dsl; + this.custom = custom; } /** 获取当前节点的 xml 标签 */ @@ -114,39 +123,46 @@ public class XmlTagInfo { } /** - * 判断当前节点上的指定属性是否为 *.xdef 的声明属性 + * 判断当前节点上的指定属性是否为 XDef 元模型的元属性(即,定义属性名及其类型) *

- * 也就是,在 *.xdef 中是在定义该属性及其类型,而不是为该属性赋值。 - * 在 xdef.xdef 这类自举定义的 xdsl 中,会通过不同的名字空间来区分属性声明和属性赋值。 - * 比如,名字空间为 meta 和 x 的属性则为赋值属性,其余(无名字空间和 xdef 名字空间)的则为声明属性 + * 对于这类属性,仅做类型引用跳转,不做文件或名字引用跳转 */ - public boolean isXDefDeclaredAttr(String attrName) { + public boolean isDefDeclaredAttr(String attrName) { + // Note: + // - 自定义节点(包括 Xpl 类型节点及其子节点)没有元模型 + // - 在 DSL 节点上的属性也不是元属性 + if (custom || dsl) { + return false; + } + + // 检查在 *.xdef 节点上的元属性 String ns = StringHelper.getNamespace(attrName); + if (StringHelper.isEmpty(ns)) { + return true; + } - return StringHelper.isEmpty(ns) || (!ns.equals(xdefNs) && !ns.equals(xdslNs) && !ns.equals("xmlns")); + // xdef.xdef 中 xdef 名字空间的属性均视为元属性 + if (isXDefNode()) { + return "xdef".equals(ns); + } + // xdsl.xdef 中 x 名字空间的属性均视为元属性 + else if (isXDslNode()) { + return "x".equals(ns); + } + // 对于普通的 *.xdef,除 xmlns、xdef、x 名字空间以外的属性,均视为元属性 + return !ns.equals("xdef") && !ns.equals("x") && !ns.equals("xmlns"); } - /** - * 获取当前节点上指定属性的 xdef 定义 - *

    - *
  • xdef.xdef 节点属性上的 meta 名字空间自动转换为 xdef
  • - *
  • xdsl.xdef 节点属性上的 xdsl 名字空间自动转换为 x
  • - *
- */ - public IXDefAttribute getAttr(String attrName) { - // TODO xpl 名字空间的节点定义在 xpl.xdef 中 - attrName = XDefPsiHelper.normalizeNamespace(attrName, xdefNs, xdslNs); + /** 获取当前节点上指定属性的 xdef 定义 */ + public IXDefAttribute getDefAttr(String attrName) { + DefAttrWithNode attr = getDefAttrInfo(attrName); - IXDefAttribute attr = defNode != null ? defNode.getAttribute(attrName) : null; - if (attr == null) { - attr = xdslDefNode != null ? xdslDefNode.getAttribute(attrName) : null; - } - return attr; + return attr != null ? attr.attr : null; } /** 获取当前节点上指定属性的类型 */ - public XDefTypeDecl getAttrType(String attrName) { - IXDefAttribute attr = getAttr(attrName); + public XDefTypeDecl getDefAttrType(String attrName) { + IXDefAttribute attr = getDefAttr(attrName); if (attr == null) { return defNode != null ? defNode.getXdefUnknownAttr() : null; @@ -155,27 +171,84 @@ public class XmlTagInfo { } /** 获取节点注释 */ - public IXDefComment getComment() { + public IXDefComment getDefNodeComment() { return defNode != null ? defNode.getComment() : null; } - /** - * 获取指定属性的注释 - *
    - *
  • xdef.xdef 节点属性上的 meta 名字空间自动转换为 xdef
  • - *
  • xdsl.xdef 节点属性上的 xdsl 名字空间自动转换为 x
  • - *
- */ - public IXDefSubComment getAttrComment(String attrName) { - attrName = XDefPsiHelper.normalizeNamespace(attrName, xdefNs, xdslNs); + /** 获取指定属性的注释 */ + public IXDefSubComment getDefAttrComment(String attrName) { + if (isXmlns(attrName)) { + return null; + } IXDefComment comment; - if (defNode == null || defNode.getAttribute(attrName) == null) { - comment = xdslDefNode != null ? xdslDefNode.getComment() : null; + DefAttrWithNode attr = getDefAttrInfo(attrName); + if (attr == null) { + comment = getDefNodeComment(); + // 若无属性实体,则取当前节点上的 xdef:unknown-attr 属性的注释 + attrName = "xdef:unknown-attr"; } else { - comment = getComment(); + comment = attr.node.getComment(); + attrName = attr.attr.getName(); } return comment != null ? comment.getSubComments().get(attrName) : null; } + + private boolean isXmlns(String name) { + return name.equals("xmlns") || name.startsWith("xmlns:"); + } + + /** 是否为 xdef.xdef 中的节点 */ + private boolean isXDefNode() { + return xdefNs != null && !"xdef".equals(xdefNs); + } + + /** 是否为 xdsl.xdef 中的节点 */ + private boolean isXDslNode() { + return xdslNs != null && !"x".equals(xdslNs); + } + + /** 记录属性所在节点 */ + record DefAttrWithNode(IXDefNode node, IXDefAttribute attr) {} + + /** 获取当前节点上指定属性的定义信息 */ + private DefAttrWithNode getDefAttrInfo(String attrName) { + // 为 xmlns 节点构造属性 + if (isXmlns(attrName)) { + XDefTypeDecl type = new XDefTypeDeclParser().parseFromText(null, XDefConstants.STD_DOMAIN_XDEF_REF); + + XDefAttribute attr = new XDefAttribute(); + attr.setName(attrName); + attr.setType(type); + + return defNode != null ? new DefAttrWithNode(defNode, attr) : null; + } + + attrName = XDefPsiHelper.normalizeNamespace(attrName, xdefNs, xdslNs); + + // 查找在当前节点上声明的属性 + IXDefAttribute attr = defNode != null ? defNode.getAttribute(attrName) : null; + if (attr != null) { + return new DefAttrWithNode(defNode, attr); + } + + // 查找在对应的 xdsl.xdef 节点上声明的属性 + attr = xdslDefNode != null ? xdslDefNode.getAttribute(attrName) : null; + if (attr != null) { + return new DefAttrWithNode(xdslDefNode, attr); + } + + // 针对 xdef.xdef 中的未确定属性:本质上都是 XDefNode 节点上的属性 + if (isXDefNode()) { + IXDefNode node = def.getXdefUnknownTag(); + + return new DefAttrWithNode(node, node.getAttribute(attrName)); + } + + // Note: xdef:unknown-attr 只记录了类型,没有 IXDefAttribute 实体, + // 其处理逻辑见 XDefinitionParser#parseNode + return null; + } + } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index e839f7276..439958df7 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -6,6 +6,7 @@ import com.intellij.codeInsight.documentation.DocumentationManager; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.psi.PsiElement; import io.nop.idea.plugin.BaseXLangPluginTestCase; +import io.nop.xlang.xdef.XDefConstants; /** * 参考 https://github.com/JetBrains/intellij-community/blob/master/xml/tests/src/com/intellij/html/HtmlDocumentationTest.java @@ -34,11 +35,23 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { """, "

child



This is child node

\n"); // 显示属性文档 + // - 确定属性 doTest(""" me="Child"/> """, "

name

stdDomain: string



This is child name

\n"); + doTest(""" + + pe="leaf"/> + + """, "

type

stdDomain: dict:test/doc/child-type

"); + // - 未确定属性 + doTest(""" + + e="22"/> + + """, "

age

stdDomain: any



This a unknown attribute

\n"); // 显示 xdef.xdef 中的 meta:xxx 标签和属性文档 doTest(readVfsResource("/nop/schema/xdef.xdef").replace("nown-tag "), @@ -46,12 +59,58 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", "meta:ref=\"XDefNode\""), (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xmlns:meta", "xmlns:meta"), (genDoc) -> { + assertFalse(genDoc.contains("
")); + assertTrue(genDoc.contains("xmlns:meta")); + assertTrue(genDoc.contains(XDefConstants.STD_DOMAIN_XDEF_REF)); + }); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("own-tag "), + (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("-tag meta:ref="), + (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("ef="), + (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unknown-attr=\"string\"", + "meta:unknown-attr=\"string\""), + (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), + (genDoc) -> assertFalse(genDoc.contains("
"))); // 显示 xdsl.xdef 中的标签和 meta:xxx 属性文档 doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("nown-tag "), (genDoc) -> assertTrue(genDoc.contains("
"))); doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema", "xdsl:schema"), (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("value="), + (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("-parse "), + (genDoc) -> assertTrue(genDoc.contains("
"))); + + // 显示 xpl 类型子节点文档 + doTest(readVfsResource("/test/doc/example.xdef").replace("xpl:dump=\"true\"", "xpl:dump=\"true\""), + (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(""" + + + b="/nop/core/xlib/meta-gen.xlib"/> + + + """, (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(""" + + + b="/nop/core/xlib/meta-gen.xlib"/> + + + """, (genDoc) -> assertTrue(genDoc.contains("
"))); } public void testGenerateDocForXmlAttributeValue() { @@ -59,7 +118,7 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { - """, "

leaf-Leaf Node

"); + """, "

leaf - Leaf Node

"); } private void doTest(String text, String doc) { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java index 41a82687c..1dfbfd911 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -13,7 +13,7 @@ import io.nop.idea.plugin.BaseXLangPluginTestCase; * @date 2025-06-17 */ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { - private static final String XLANG_EXT = "xlang"; + private static final String XLANG_EXT = "xgo"; @Override protected String[] getXLangFileExtensions() { @@ -77,6 +77,11 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), "DslNode"); // - 在 *.xdef 中引用外部文件 + doTest(""" + + """, "/nop/schema/xdsl.xdef"); doTest(""" st\""), "list"); doTest(readVfsResource("/test/link/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), "Get"); +// +// // TODO 对唯一键的跳转 +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", +// "meta:unique-attr=\"name\""), ""); +// doTest(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"name\"", +// "xdef:key-attr=\"name\""), ""); // 缺省:任意有效的文件均可跳转 - doTest(""" - - """, "/nop/schema/xdsl.xdef"); doTest(""" """, "/test/link/a.xlib"); @@ -134,6 +140,23 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase {
""", "/nop/core/xlib/meta-gen.xlib"); + doTest(""" + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); + +// // TODO 声明属性仅跳转到属性的类型定义上 +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", +// "xdef:ref=\"xdef-ref\""), ""); +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("-name\""), ""); +// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", "x:schema=\"v-path\""), +// ""); } public void testGetGotoDeclarationTargetsForXmlText() { diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index cd93dffce..cb08b1a96 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -2,12 +2,17 @@ - + + + + - + -- Gitee From bc9778a22061a666798fa0547d591222592593a3 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sat, 21 Jun 2025 20:17:43 +0800 Subject: [PATCH 11/82] =?UTF-8?q?nop-idea-pugin:=20=E5=B0=86=20xlib=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=20=20=E8=8A=82=E7=82=B9=E6=8C=89?= =?UTF-8?q?=E7=85=A7=20xpl=20=E7=B1=BB=E5=9E=8B=E8=AF=86=E5=88=AB=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E5=AF=B9=E5=85=B6=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=9A=84=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nop/idea/plugin/utils/XDefPsiHelper.java | 46 +++++++++++-------- .../io/nop/idea/plugin/utils/XmlTagInfo.java | 26 ++++++----- .../doc/TestXLangDocumentationProvider.java | 11 +++++ .../link/TestXLangGotoDeclarationHandler.java | 11 +++++ 4 files changed, 63 insertions(+), 31 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 493301583..0381abc0c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -150,14 +150,14 @@ public class XDefPsiHelper { String xdslNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); boolean xpl = false; - boolean dsl = !XDslConstants.XDSL_SCHEMA_XDEF.equals(schemaUrl); + boolean xlibDsl = XDslConstants.XDSL_SCHEMA_XLIB.equals(schemaUrl); XmlTagInfo tagInfo = null; for (int i = 0, n = tags.size(); i < n; i++) { XmlTag xmlTag = tags.get(i); if (i == 0) { - tagInfo = new XmlTagInfo(xmlTag, def, def.getRootNode(), null, // - xdslDefNode, xdefNs, xdslNs, dsl, false); + tagInfo = new XmlTagInfo(xmlTag, null, def, def.getRootNode(), // + xdslDefNode, xdefNs, xdslNs); } else { XmlTagInfo parentTagInfo = tagInfo; String tagName = normalizeNamespace(xmlTag.getName(), xdefNs, xdslNs); @@ -169,28 +169,30 @@ public class XDefPsiHelper { // 否则,保持在 XDef 元模型的节点定义 if (tagName.startsWith("x:") && "x".equals(xdslNs)) { defNode = xdslDefNode; - } else { - defNode = parentTagInfo.getDefNodeChild(tagName); } - - if (defNode == null && xpl) { + // Xpl 节点始终采用 xpl.xdef 元模型 + else if (xpl) { // 通过任意未定义的子节点名称,得到 xpl 的 xdef:unknown-tag 子节点定义 defNode = getXplDef().getRootNode().getChild("any"); + } else { + defNode = parentTagInfo.getDefNodeChild(tagName); } - tagInfo = new XmlTagInfo(xmlTag, - def, - defNode, - parentTagInfo.getDefNode(), - xdslDefNode, - xdefNs, - xdslNs, - dsl, - parentTagInfo.isCustom() || parentTagInfo.isSupportBody()); + tagInfo = new XmlTagInfo(xmlTag, parentTagInfo, def, defNode, xdslDefNode, xdefNs, xdslNs); if (isXplNode(defNode)) { xpl = true; } + // xlib.xdef 中的 source 标签设置为 xml 类型,是因为在获取 XplLib 模型的时候会根据 xlib.xdef 来解析, + // 但此时这个 source 段无法自动进行编译,必须结合它的 outputMode 和 attrs 配置等才能决定。 + // 因此,将其子节点同样视为 xpl 节点处理 + else if (!xpl && xlibDsl && "source".equals(tagName) // + && "xml".equals(getDefNodeType(defNode)) // + && parentTagInfo.getDefNode().isUnknownTag() // + && "tags".equals(parentTagInfo.getParentDefNode().getTagName()) // + ) { + xpl = true; + } } } return tagInfo; @@ -210,12 +212,16 @@ public class XDefPsiHelper { } static boolean isXplNode(IXDefNode defNode) { + String stdDomain = getDefNodeType(defNode); + + return stdDomain != null && (stdDomain.equals("xpl") || stdDomain.startsWith("xpl-")); + } + + static String getDefNodeType(IXDefNode defNode) { if (defNode == null || defNode.getXdefValue() == null) { - return false; + return null; } - - String stdDomain = defNode.getXdefValue().getStdDomain(); - return stdDomain.equals("xpl") || stdDomain.startsWith("xpl-"); + return defNode.getXdefValue().getStdDomain(); } /** 自底向上查找 tag 所在分支上的节点 */ diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java index 39d1f3721..6c984498f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java @@ -19,39 +19,38 @@ import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.domain.StdDomainRegistry; import io.nop.xlang.xdef.impl.XDefAttribute; import io.nop.xlang.xdef.parse.XDefTypeDeclParser; +import io.nop.xlang.xdsl.XDslConstants; public class XmlTagInfo { private final XmlTag tag; private final IXDefinition def; private final IXDefNode defNode; - private final IXDefNode parentDefNode; private final IXDefNode xdslDefNode; + private final IXDefNode parentDefNode; private final String xdefNs; private final String xdslNs; - /** 当前节点是否为 DSL 节点 */ - private final boolean dsl; private final boolean custom; public XmlTagInfo( - XmlTag tag, // - IXDefinition def, IXDefNode defNode, IXDefNode parentDefNode, // + XmlTag tag, XmlTagInfo parentTagInfo, // + IXDefinition def, IXDefNode defNode, // IXDefNode xdslDefNode, // - String xdefNs, String xdslNs, // - boolean dsl, boolean custom // + String xdefNs, String xdslNs // ) { this.tag = tag; this.def = def; this.defNode = defNode; - this.parentDefNode = parentDefNode; this.xdslDefNode = xdslDefNode; + + this.parentDefNode = parentTagInfo != null ? parentTagInfo.getDefNode() : null; this.xdefNs = xdefNs; this.xdslNs = xdslNs; - this.dsl = dsl; - this.custom = custom; + this.custom = parentTagInfo != null // + && (parentTagInfo.isCustom() || parentTagInfo.isSupportBody()); } /** 获取当前节点的 xml 标签 */ @@ -131,7 +130,7 @@ public class XmlTagInfo { // Note: // - 自定义节点(包括 Xpl 类型节点及其子节点)没有元模型 // - 在 DSL 节点上的属性也不是元属性 - if (custom || dsl) { + if (isCustom() || isDslNode()) { return false; } @@ -199,6 +198,11 @@ public class XmlTagInfo { return name.equals("xmlns") || name.startsWith("xmlns:"); } + /** 当前节点是否为 DSL 节点 */ + private boolean isDslNode() { + return !XDslConstants.XDSL_SCHEMA_XDEF.equals(def.resourcePath()); + } + /** 是否为 xdef.xdef 中的节点 */ private boolean isXDefNode() { return xdefNs != null && !"xdef".equals(xdefNs); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index 439958df7..febe0e05b 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -111,6 +111,17 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { """, (genDoc) -> assertTrue(genDoc.contains("
"))); + doTest(""" + + + + + b="/nop/core/xlib/meta-gen.xlib"/> + + + + + """, (genDoc) -> assertTrue(genDoc.contains("
"))); } public void testGenerateDocForXmlAttributeValue() { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java index 1dfbfd911..b2a5df825 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -149,6 +149,17 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { """, "/nop/core/xlib/meta-gen.xlib"); + doTest(""" + + + + + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); // // TODO 声明属性仅跳转到属性的类型定义上 // doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", -- Gitee From 9592300687fe3d5d7a320bbcd62b5074bb464cfd Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 22 Jun 2025 09:51:29 +0800 Subject: [PATCH 12/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AF=B9=20xdef:key-attr=E3=80=81xdef:unique-attr=20=E6=89=80?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E5=B1=9E=E6=80=A7=E7=9A=84=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../link/XLangGotoDeclarationHandler.java | 23 +++- .../nop/idea/plugin/utils/XmlPsiHelper.java | 111 ++++++++++++------ .../idea/plugin/BaseXLangPluginTestCase.java | 29 ++++- .../doc/TestXLangDocumentationProvider.java | 13 +- .../link/TestXLangGotoDeclarationHandler.java | 86 ++++++++++---- 5 files changed, 193 insertions(+), 69 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java index a5e8bf00f..243a5f52a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -121,10 +121,11 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); } else { String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); + if ((xdefNs + ":key-attr").equals(attrName)) { - // + return getGotoDeclarationTargetsFromKeyAttr(project, tagInfo, attrValue); } else if ((xdefNs + ":unique-attr").equals(attrName)) { - // + return getGotoDeclarationTargetsFromUniqueAttr(project, tagInfo, attrValue); } } } @@ -256,6 +257,24 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return protoTag != null ? new PsiElement[] { protoTag } : null; } + /** 从 xdef:key-attr 的属性值中获得跳转元素(节点属性) */ + private PsiElement[] getGotoDeclarationTargetsFromKeyAttr( + Project project, XmlTagInfo tagInfo, String attrValue + ) { + return XmlPsiHelper.getAttrsFromChildTag(tagInfo.getTag(), attrValue); + } + + /** 从 xdef:unique-attr 的属性值中获得跳转元素(节点属性) */ + private PsiElement[] getGotoDeclarationTargetsFromUniqueAttr( + Project project, XmlTagInfo tagInfo, String attrValue + ) { + // 仅从当前节点中取引用到的属性 + XmlTag tag = tagInfo.getTag(); + XmlAttribute attr = tag.getAttribute(attrValue); + + return attr != null ? new PsiElement[] { attr } : null; + } + /** 从 csv 中提取指定偏移位置所在的文件路径 */ private String extractPathFromCsv(String csv, int offset) { int start = offset; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index ef4b0634c..2981fbd88 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -7,6 +7,14 @@ */ package io.nop.idea.plugin.utils; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + import com.intellij.lang.ASTNode; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; @@ -28,14 +36,6 @@ import io.nop.commons.util.StringHelper; import io.nop.core.resource.ResourceHelper; import io.nop.xlang.xpl.xlib.XplLibHelper; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - public class XmlPsiHelper { static final PsiFile[] EMPTY_FILES = new PsiFile[0]; static final PsiElement[] EMPTY_ELEMENTS = new PsiElement[0]; @@ -48,24 +48,27 @@ public class XmlPsiHelper { public static List findPsiFileList(Project project, String path) { String fileName = StringHelper.fileFullName(path); PsiFile[] files = FilenameIndex.getFilesByName(project, fileName, GlobalSearchScope.allScope(project)); - if (files.length == 0) + if (files.length == 0) { return Collections.emptyList(); + } path = ResourceHelper.getStdPath(path); List ret = new ArrayList<>(files.length); for (PsiFile file : files) { String matchPath = ProjectFileHelper.getNopVfsStdPath(file.getVirtualFile()); - if (Objects.equals(path, matchPath)) + if (Objects.equals(path, matchPath)) { ret.add(file); + } } return ret; } public static PsiFile[] findPsiFile(Project project, String path) { List list = findPsiFileList(project, path); - if (list.isEmpty()) + if (list.isEmpty()) { return EMPTY_FILES; + } return list.toArray(EMPTY_FILES); } @@ -78,8 +81,9 @@ public class XmlPsiHelper { // 同一个库文件可能存在多个定制文件 String path = ProjectFileHelper.getNopVfsPath(file.getVirtualFile()); List list = findPsiFileList(project, path); - if (list.isEmpty()) + if (list.isEmpty()) { list = Collections.singletonList(file); + } return list; } @@ -101,8 +105,9 @@ public class XmlPsiHelper { List paths = StringHelper.split(attr.getValue(), ','); for (String path : paths) { String libNs = XplLibHelper.getNamespaceFromLibPath(path); - if (ns.equals(libNs)) + if (ns.equals(libNs)) { return findPsiFileList(project, path); + } } } @@ -112,8 +117,9 @@ public class XmlPsiHelper { public static PsiElement[] findXplTag(Project project, XmlTag tag) { List files = findXplLib(project, tag); - if (files.isEmpty()) + if (files.isEmpty()) { return EMPTY_ELEMENTS; + } String tagBegin = "<" + tag.getLocalName(); @@ -125,7 +131,10 @@ public class XmlPsiHelper { int pos = text.indexOf(tagBegin, fromPos); if (pos >= 0) { int end = pos + tagBegin.length(); - if (end == text.length() || text.charAt(end) == ' ' || text.charAt(end) == '/' || text.charAt(end) == '>') { + if (end == text.length() + || text.charAt(end) == ' ' + || text.charAt(end) == '/' + || text.charAt(end) == '>') { PsiElement element = file.findElementAt(pos + 1); if (element != null && isXmlTag(element)) { ret.add(element); @@ -148,12 +157,14 @@ public class XmlPsiHelper { public static String getNopVfsPath(PsiElement element) { PsiFile file = element.getContainingFile(); - if (file == null) + if (file == null) { return null; + } VirtualFile vf = file.getVirtualFile(); - if (vf == null) + if (vf == null) { return null; + } return ProjectFileHelper.getNopVfsPath(vf); } @@ -163,8 +174,9 @@ public class XmlPsiHelper { } public static SourceLocation getValueLocation(XmlTag element) { - if (element.getValue() == null) + if (element.getValue() == null) { return null; + } TextRange range = element.getValue().getTextRange(); int offset = range.getStartOffset(); @@ -174,8 +186,9 @@ public class XmlPsiHelper { static SourceLocation getLocation(PsiElement element, int offset, int len) { PsiFile psiFile = element.getContainingFile(); VirtualFile vf = psiFile.getVirtualFile(); - if (vf == null) + if (vf == null) { return null; + } String path = ProjectFileHelper.getNopVfsPath(vf); @@ -189,29 +202,34 @@ public class XmlPsiHelper { private static PsiElement getRootElement(PsiElement element) { do { PsiElement parent = element.getParent(); - if (parent == null) + if (parent == null) { return element; + } element = parent; } while (true); } public static String getAttrName(XmlAttributeValue value) { - if (value == null) + if (value == null) { return null; + } - if (!(value.getParent() instanceof XmlAttribute)) + if (!(value.getParent() instanceof XmlAttribute)) { return null; + } XmlAttribute attr = (XmlAttribute) value.getParent(); return attr.getName(); } public static XmlAttribute getAttr(XmlAttributeValue value) { - if (value == null) + if (value == null) { return null; + } - if (!(value.getParent() instanceof XmlAttribute)) + if (!(value.getParent() instanceof XmlAttribute)) { return null; + } XmlAttribute attr = (XmlAttribute) value.getParent(); return attr; @@ -224,16 +242,18 @@ public class XmlPsiHelper { public static boolean isInComment(PsiElement element) { IElementType elementType = element.getNode().getElementType(); return elementType == XmlTokenType.XML_COMMENT_END - || elementType == XmlTokenType.XML_COMMENT_START - || elementType == XmlTokenType.XML_COMMENT_CHARACTERS; + || elementType == XmlTokenType.XML_COMMENT_START + || elementType == XmlTokenType.XML_COMMENT_CHARACTERS; } public static boolean isElementType(PsiElement elm, IElementType type) { - if (elm == null) + if (elm == null) { return false; + } ASTNode node = elm.getNode(); - if (node == null) + if (node == null) { return false; + } return node.getElementType() == type; } @@ -270,19 +290,40 @@ public class XmlPsiHelper { return null; } + /** 根据查找子节点上指定名字的属性 */ + public static XmlAttribute[] getAttrsFromChildTag(XmlTag tag, String attrName) { + List attrs = new ArrayList<>(); + + for (PsiElement element : tag.getChildren()) { + if (!(element instanceof XmlTag child)) { + continue; + } + + XmlAttribute attr = child.getAttribute(attrName); + if (attr != null) { + attrs.add(attr); + } + } + return attrs.isEmpty() ? null : attrs.toArray(new XmlAttribute[0]); + } + public static XmlTag getXmlTag(PsiElement element) { - if (element == null) + if (element == null) { return null; + } - if (element instanceof XmlTag) + if (element instanceof XmlTag) { return ((XmlTag) element); + } do { PsiElement parent = element.getParent(); - if (parent == null) + if (parent == null) { return null; - if (parent instanceof XmlTag) + } + if (parent instanceof XmlTag) { return (XmlTag) parent; + } element = parent; } while (true); } @@ -290,8 +331,9 @@ public class XmlPsiHelper { public static XmlTag getRoot(XmlTag tag) { do { XmlTag parent = tag.getParentTag(); - if (parent == null) + if (parent == null) { return tag; + } tag = parent; } while (true); } @@ -300,8 +342,9 @@ public class XmlPsiHelper { for (XmlAttribute attr : tag.getAttributes()) { if (url.equals(attr.getValue())) { String name = attr.getName(); - if (name.startsWith("xmlns:")) + if (name.startsWith("xmlns:")) { return name.substring("xmlns:".length()); + } } } return null; diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index ce66b3e53..a72152ebe 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -1,7 +1,11 @@ package io.nop.idea.plugin; +import com.intellij.codeInsight.documentation.DocumentationManager; +import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; +import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.psi.PsiElement; import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; import io.nop.api.core.ApiConfigs; import io.nop.api.core.config.AppConfig; @@ -67,7 +71,11 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur return new String[0]; } - /** 将测试环境中的 vfs 资源添加到 Project 中 */ + /** + * 将测试环境中的 vfs 资源添加到 Project 中 + *

+ * 在需要做文件跳转时,需要将目标文件提前加入 Project 以确保目标文件已存在 + */ protected void addVfsResourcesToProject(String... resources) { for (String resource : resources) { String text = readVfsResource(resource); @@ -80,4 +88,23 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur IResource res = VirtualFileSystem.instance().getResource(resource); return ResourceHelper.readText(res); } + + protected String getDoc() { + // Note: 通过 ApplicationManager.getApplication().runReadAction(() -> {}) + // 消除异常 "Read access is allowed from inside read-action" + PsiElement originalElement = myFixture.getFile() + .findElementAt(myFixture.getEditor().getCaretModel().getOffset()); + PsiElement element = DocumentationManager.getInstance(getProject()) + .findTargetElement(myFixture.getEditor(), myFixture.getFile()); + + DocumentationProvider docProvider = DocumentationManager.getProviderFromElement(originalElement); + + return docProvider.generateDoc(element, originalElement); + } + + protected PsiElement[] getGotoTargets() { + return GotoDeclarationAction.findAllTargetElements(getProject(), + myFixture.getEditor(), + myFixture.getCaretOffset()); + } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index febe0e05b..fd5dc506e 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -2,9 +2,6 @@ package io.nop.idea.plugin.doc; import java.util.function.Consumer; -import com.intellij.codeInsight.documentation.DocumentationManager; -import com.intellij.lang.documentation.DocumentationProvider; -import com.intellij.psi.PsiElement; import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.xlang.xdef.XDefConstants; @@ -140,15 +137,7 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { private void doTest(String text, Consumer checker) { myFixture.configureByText("example." + XLANG_EXT, text); - // Note: 通过 ApplicationManager.getApplication().runReadAction(() -> {}) - // 消除异常 "Read access is allowed from inside read-action" - PsiElement originalElement = myFixture.getFile() - .findElementAt(myFixture.getEditor().getCaretModel().getOffset()); - PsiElement element = DocumentationManager.getInstance(getProject()) - .findTargetElement(myFixture.getEditor(), myFixture.getFile()); - - DocumentationProvider docProvider = DocumentationManager.getProviderFromElement(originalElement); - String genDoc = docProvider.generateDoc(element, originalElement); + String genDoc = getDoc(); checker.accept(genDoc); } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java index b2a5df825..4226ae445 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -1,7 +1,7 @@ package io.nop.idea.plugin.link; -import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import io.nop.idea.plugin.BaseXLangPluginTestCase; @@ -24,6 +24,7 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { protected void setUp() throws Exception { super.setUp(); + // Note: 提前将需要跳转的文件添加到 Project 中 addVfsResourcesToProject("/nop/schema/xdef.xdef", "/nop/schema/xdsl.xdef", "/nop/schema/xmeta.xdef", @@ -31,6 +32,7 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { "/nop/schema/xui/store.xdef", "/nop/core/xlib/meta-gen.xlib", "/nop/schema/schema/obj-schema.xdef", + "/dict/test/doc/child-type.dict.yaml", "/test/link/a.xmeta", "/test/link/b.xmeta", "/test/link/a.xlib", @@ -49,16 +51,6 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { x:extends="/test/link/a.xmeta" /> """, "/test/link/a.xmeta"); - doTest(""" - - """, "/test/link/b.xmeta"); - doTest(""" - - """, "/test/link/a.xmeta"); // 对 xdef.xdef 中引用的跳转 doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", @@ -101,12 +93,14 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { "x:prototype=\"list\""), "list"); doTest(readVfsResource("/test/link/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), "Get"); -// -// // TODO 对唯一键的跳转 -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", -// "meta:unique-attr=\"name\""), ""); -// doTest(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"name\"", -// "xdef:key-attr=\"name\""), ""); + + // 对唯一键的跳转 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", + "meta:unique-attr=\"name\""), "name"); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"xdef:name\"", + "meta:unique-attr=\"xdef:name\""), "xdef:name"); + doTest(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"name\"", + "xdef:key-attr=\"name\""), "name"); // 缺省:任意有效的文件均可跳转 doTest(""" @@ -170,6 +164,55 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { // ""); } + public void testGetGotoDeclarationTargetsForXmlAttributePartialValue() { + // v-path-list 元素跳转 + doTest(""" + + """, "/test/link/b.xmeta"); + doTest(""" + + """, "/test/link/a.xmeta"); + + // TODO 字典/枚举的 options 跳转 + doTest(""" + + + + """, "/dict/test/doc/child-type.dict.yaml"); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + "io.nop.xlang.xdef.XDefOverride"); + + // TODO 字典/枚举的默认值跳转 + doTest(""" + + + + """, ""); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + ""); + + // TODO 缺省属性值中 @attr: 引用跳转 + doTest(""" + + + + """, ""); + } + public void testGetGotoDeclarationTargetsForXmlText() { } @@ -177,9 +220,7 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { private void doTest(String text, String... expected) { myFixture.configureByText("example." + XLANG_EXT, text); - PsiElement[] refs = GotoDeclarationAction.findAllTargetElements(getProject(), - myFixture.getEditor(), - myFixture.getCaretOffset()); + PsiElement[] refs = getGotoTargets(); assertNotNull(refs); assertEquals(refs.length, expected.length); @@ -214,6 +255,11 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { assertEquals(exp, actual); } + // 引用属性 + else if (ref instanceof XmlAttribute attr) { + String actual = attr.getName(); + assertEquals(exp, actual); + } } } } -- Gitee From 93396c31600dc78544cc5ca3ba7f6b18c633975c Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Mon, 23 Jun 2025 10:54:22 +0800 Subject: [PATCH 13/82] =?UTF-8?q?nop-idea-pugin:=20=E5=88=9D=E6=AD=A5?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=80=9A=E8=BF=87=20PsiReference=20=E6=9B=BF?= =?UTF-8?q?=E4=BB=A3=20GotoDeclaration=EF=BC=8C=E4=BB=8E=E8=80=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=BB=A3=E7=A0=81=E8=A1=A5=E5=85=A8=E3=80=81=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E8=B7=B3=E8=BD=AC=E3=80=81=E5=BC=95=E7=94=A8=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=AD=89=E6=9B=B4=E5=A4=9A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 18 +- .../plugin/reference/NopVfsFileReference.java | 69 +++++++ .../reference/XLangReferenceContributor.java | 43 +++++ .../reference/XLangReferenceProvider.java | 178 ++++++++++++++++++ .../nop/idea/plugin/utils/XmlPsiHelper.java | 29 +-- .../src/main/resources/META-INF/plugin.xml | 4 +- .../idea/plugin/BaseXLangPluginTestCase.java | 14 +- .../reference/TestXLangReferenceProvider.java | 47 +++++ 8 files changed, 385 insertions(+), 17 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceContributor.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index 7d6bcff7a..ae1788d95 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -11,6 +11,7 @@ import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.Annotator; import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; import com.intellij.psi.PsiElement; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; @@ -25,6 +26,7 @@ import io.nop.commons.util.StringHelper; import io.nop.core.dict.DictProvider; import io.nop.core.exceptions.ErrorMessageManager; import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.reference.NopVfsFileReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -42,12 +44,26 @@ public class XLangAnnotator implements Annotator { @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { ProjectEnv.withProject(element.getProject(), () -> { - doAnnotate(element, holder); + try { + doAnnotate(element, holder); + } catch (Exception e) { + holder.newAnnotation(HighlightSeverity.WARNING, e.getMessage()) + .highlightType(ProblemHighlightType.WARNING) + .create(); + } return null; }); } void doAnnotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (element.getReference() instanceof NopVfsFileReference) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(element.getReference().getAbsoluteRange()) + .textAttributes(DefaultLanguageHighlighterColors.CLASS_REFERENCE) + .create(); + return; + } + if (element instanceof XmlTag) { XmlTag tag = (XmlTag) element; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java new file mode 100644 index 000000000..ad19abeae --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java @@ -0,0 +1,69 @@ +package io.nop.idea.plugin.reference; + +import java.util.ArrayList; +import java.util.List; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlElement; +import com.intellij.psi.xml.XmlElementType; +import com.intellij.psi.xml.XmlTag; +import com.intellij.util.ArrayUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对 Nop vfs 资源文件的引用 + * + * @author flytreeleft + * @date 2025-06-22 + */ +public class NopVfsFileReference extends PsiReferenceBase { + private final PsiFile file; + + /** + * @param refElement + * 与引用直接相关的元素。例如绑定属性值与文件的引用关系,则该参数需为不包含引号的、类型为 + * {@link XmlElementType#XML_ATTRIBUTE_VALUE_TOKEN} 类型的元素。 + * 如果在该元素内以分隔符分隔了多个引用,则需要通过参数 textRange + * 指定对应引用的关联文本所在的文本范围,该范围相对于该元素,从 0 开始计算。 + * 相关处理逻辑见 {@link com.intellij.psi.impl.SharedPsiElementImplUtil#addReferences SharedPsiElementImplUtil#addReferences}) + */ + public NopVfsFileReference(@NotNull XmlElement refElement, TextRange textRange, @NotNull PsiFile file) { + super(refElement, textRange, + // 不采用延迟解析模式,以确保当解析到有其他相关引用时,其能够被 PsiMultiReference 作为最优引用 + false); + this.file = file; + } + + /** 得到具体的引用对象(文件、文件行、文件内某个元素等) */ + @Override + public @Nullable PsiElement resolve() { + // TODO 在光标显示引用信息时,参考 class 文档样式显示 xdef/dsl 的 vfs 路径及其文档:JavaDocumentationProvider + List results = new ArrayList<>(); + PsiTreeUtil.processElements(this.file, el -> { + if (el instanceof XmlTag tag) { + results.add(tag); + // 仅取根节点 + return false; + } + return true; // 继续遍历 + }); + + return results.isEmpty() ? this.file : results.get(0); + } + + /** 得到补全建议元素列表,可以为字符串或 {@link PsiElement} */ + @Override + public @NotNull Object @NotNull [] getVariants() { + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + @Override + public boolean isReferenceTo(@NotNull PsiElement target) { + return target instanceof PsiFile && ((PsiFile) target).getVirtualFile().getPath().endsWith("/_vfs" + file); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceContributor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceContributor.java new file mode 100644 index 000000000..a51d2157f --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceContributor.java @@ -0,0 +1,43 @@ +package io.nop.idea.plugin.reference; + +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.patterns.ElementPattern; +import com.intellij.patterns.ObjectPattern; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.patterns.PsiFilePattern; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReferenceContributor; +import com.intellij.psi.PsiReferenceRegistrar; +import io.nop.idea.plugin.lang.XLangFileType; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.psi.PsiReferenceRegistrar.HIGHER_PRIORITY; + +/** + * 对 XLang 中的引用进行识别 + * + * @author flytreeleft + * @date 2025-06-22 + */ +public class XLangReferenceContributor extends PsiReferenceContributor { + + @Override + public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { + ElementPattern pattern = PlatformPatterns.psiElement() + .inFile(withFileType(XLangFileType.class)); + + // Note: 对 XLang 文件的引用解析采用最高优先级,确保在 PsiMultiReference 中将解析到的 XLang 引用作为优先引用 + registrar.registerReferenceProvider(pattern, new XLangReferenceProvider(), HIGHER_PRIORITY); + } + + static PsiFilePattern withFileType(Class type) { + return PlatformPatterns.psiFile().withFileType(new FileTypePattern<>(type)); + } + + static class FileTypePattern extends ObjectPattern> { + + protected FileTypePattern(@NotNull Class aClass) { + super(aClass); + } + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java new file mode 100644 index 000000000..44890b8f0 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -0,0 +1,178 @@ +package io.nop.idea.plugin.reference; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceProvider; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlAttributeValue; +import com.intellij.psi.xml.XmlElement; +import com.intellij.psi.xml.XmlElementType; +import com.intellij.psi.xml.XmlTag; +import com.intellij.util.ProcessingContext; +import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.idea.plugin.utils.XDefPsiHelper; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.utils.XmlTagInfo; +import io.nop.xlang.xdef.XDefTypeDecl; +import org.jetbrains.annotations.NotNull; + +import static io.nop.idea.plugin.utils.XmlPsiHelper.isElementType; + +/** + * 针对 XLang 中的 {@link PsiElement 元素} 创建引用 + * + * @author flytreeleft + * @date 2025-06-22 + */ +public class XLangReferenceProvider extends PsiReferenceProvider { + + /** + * @param element + * 在探测的元素,若针对该元素返回了非空结果,则将该引用与 element 建立关联。 + * 因此,需要精确针对某个 element,而不能包含多余内容,从而确保在 UI 层面, + * 高亮的或可点击的部分只是该 element,而不会包含引号等无关内容 + */ + @Override + public PsiReference @NotNull [] getReferencesByElement( + @NotNull PsiElement element, @NotNull ProcessingContext context + ) { + Project project = element.getProject(); + + return ProjectEnv.withProject(project, () -> { + /* XmlTag:xdef:unknown-tag(505,3305) + XmlToken:XML_START_TAG_START('<')(505,506) + XmlToken:XML_NAME('xdef:unknown-tag')(506,522) + PsiElement(XML_ATTRIBUTE)(523,558) + XmlToken:XML_NAME('xdsl:schema')(523,534) + XmlToken:XML_EQ('=')(534,535) + PsiElement(XML_ATTRIBUTE_VALUE)(535,558) + XmlToken:XML_ATTRIBUTE_VALUE_START_DELIMITER('"')(535,536) + XmlToken:XML_ATTRIBUTE_VALUE_TOKEN('/nop/schema/xdef.xdef')(536,557) + XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"')(557,558) + */ + if (isElementType(element, XmlElementType.XML_NAME)) { + XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); + + if (attr != null) { + return getReferencesFromXmlAttribute(project, (XmlElement) element, attr); + } else { + XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); + + if (tag != null) { + return getReferencesFromXmlTag(project, (XmlElement) element, tag); + } + } + } // + else if (isElementType(element, XmlElementType.XML_ATTRIBUTE_VALUE)) { + XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); + + if (attr != null) { + return getReferencesFromXmlAttributeValue(project, (XmlAttributeValue) element, attr); + } + } // + else if (isElementType(element, XmlElementType.XML_TEXT)) { + XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); + + if (tag != null) { + return getReferencesFromXmlText(project, (XmlElement) element, tag); + } + } + + return PsiReference.EMPTY_ARRAY; + }); + } + + private PsiReference @NotNull [] getReferencesFromXmlTag(Project project, XmlElement refElement, XmlTag tag) { + return PsiReference.EMPTY_ARRAY; + } + + private PsiReference @NotNull [] getReferencesFromXmlAttribute( + Project project, XmlElement refElement, XmlAttribute attr + ) { + return PsiReference.EMPTY_ARRAY; + } + + private PsiReference @NotNull [] getReferencesFromXmlAttributeValue( + Project project, XmlAttributeValue refElement, XmlAttribute attr + ) { + // Note: XmlAttributeValue#getValue 的结果包含引号 + String attrValue = attr.getValue(); + if (StringHelper.isEmpty(attrValue)) { + return PsiReference.EMPTY_ARRAY; + } + + String attrName = attr.getName(); + + XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); + XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getDefAttrType(attrName) : null; + // 在无节点定义时,仅做缺省处理 + if (attrDefType == null) { + return getReferencesByPath(project, refElement, attrValue); + } + + // TODO 对于声明属性,仅对其类型的定义(涉及枚举和字典)做跳转 + if (tagInfo.isDefDeclaredAttr(attrName)) { + return PsiReference.EMPTY_ARRAY; + } + + // 根据属性声明的类型,对属性值做文件/名字引用跳转处理 + PsiFile file = attr.getContainingFile(); + String stdDomain = attrDefType.getStdDomain(); + +// // Note: v-path 类型采用缺省处理 +// if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { +// return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); +// } // +// else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { +// return getGotoDeclarationTargetsFromXDefRef(project, attr, attrValue); +// } // +// else { +// String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); +// +// if ((xdslNs + ":prototype").equals(attrName)) { +// return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); +// } else { +// String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); +// +// if ((xdefNs + ":key-attr").equals(attrName)) { +// return getGotoDeclarationTargetsFromKeyAttr(project, tagInfo, attrValue); +// } else if ((xdefNs + ":unique-attr").equals(attrName)) { +// return getGotoDeclarationTargetsFromUniqueAttr(project, tagInfo, attrValue); +// } +// } +// } + + // 缺省引用识别 + // + // + //

+ return getReferencesByPath(project, refElement, attrValue); + } + + private PsiReference[] getReferencesFromXmlText(Project project, XmlElement refElement, XmlTag tag) { + return PsiReference.EMPTY_ARRAY; + } + + /** 获取指定路径的引用(文件) */ + private PsiReference[] getReferencesByPath(Project project, @NotNull XmlAttributeValue refElement, String path) { + if (!StringHelper.isValidFilePath(path)) { + return PsiReference.EMPTY_ARRAY; + } + + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange textRange = new TextRange(1, path.length() + 1); + String absPath = XmlPsiHelper.absolutePath(path, refElement); + + return XmlPsiHelper.findPsiFileList(project, absPath) + .stream() + .map((file) -> new NopVfsFileReference(refElement, textRange, file)) + .toArray(PsiReference[]::new); + } +} + + diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 2981fbd88..e31f4c547 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -9,6 +9,7 @@ package io.nop.idea.plugin.utils; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -22,6 +23,7 @@ import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; @@ -37,8 +39,6 @@ import io.nop.core.resource.ResourceHelper; import io.nop.xlang.xpl.xlib.XplLibHelper; public class XmlPsiHelper { - static final PsiFile[] EMPTY_FILES = new PsiFile[0]; - static final PsiElement[] EMPTY_ELEMENTS = new PsiElement[0]; public static String absolutePath(String path, XmlElement element) { String filePath = getNopVfsPath(element); @@ -47,18 +47,21 @@ public class XmlPsiHelper { public static List findPsiFileList(Project project, String path) { String fileName = StringHelper.fileFullName(path); - PsiFile[] files = FilenameIndex.getFilesByName(project, fileName, GlobalSearchScope.allScope(project)); - if (files.length == 0) { + Collection vfList = FilenameIndex.getVirtualFilesByName(fileName, + GlobalSearchScope.allScope(project)); + if (vfList.isEmpty()) { return Collections.emptyList(); } path = ResourceHelper.getStdPath(path); - List ret = new ArrayList<>(files.length); - for (PsiFile file : files) { - String matchPath = ProjectFileHelper.getNopVfsStdPath(file.getVirtualFile()); - if (Objects.equals(path, matchPath)) { - ret.add(file); + List ret = new ArrayList<>(vfList.size()); + for (VirtualFile vf : vfList) { + String vfPath = ProjectFileHelper.getNopVfsStdPath(vf); + + if (Objects.equals(path, vfPath)) { + PsiFile f = PsiManager.getInstance(project).findFile(vf); + ret.add(f); } } return ret; @@ -67,9 +70,9 @@ public class XmlPsiHelper { public static PsiFile[] findPsiFile(Project project, String path) { List list = findPsiFileList(project, path); if (list.isEmpty()) { - return EMPTY_FILES; + return PsiFile.EMPTY_ARRAY; } - return list.toArray(EMPTY_FILES); + return list.toArray(PsiFile.EMPTY_ARRAY); } public static List findXplLib(Project project, XmlTag tag) { @@ -118,7 +121,7 @@ public class XmlPsiHelper { public static PsiElement[] findXplTag(Project project, XmlTag tag) { List files = findXplLib(project, tag); if (files.isEmpty()) { - return EMPTY_ELEMENTS; + return PsiElement.EMPTY_ARRAY; } String tagBegin = "<" + tag.getLocalName(); @@ -147,7 +150,7 @@ public class XmlPsiHelper { } } while (true); } - return ret.toArray(EMPTY_ELEMENTS); + return ret.toArray(PsiElement.EMPTY_ARRAY); } private static boolean isXmlTag(PsiElement element) { diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 31e4116c1..b8da40f26 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -84,8 +84,10 @@ + + - + diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index a72152ebe..7c85784f6 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -1,11 +1,13 @@ package io.nop.idea.plugin; +import com.intellij.codeInsight.TargetElementUtil; import com.intellij.codeInsight.documentation.DocumentationManager; import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; import io.nop.api.core.ApiConfigs; import io.nop.api.core.config.AppConfig; @@ -89,11 +91,19 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur return ResourceHelper.readText(res); } + protected PsiElement getElementAtCaret() { + return myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + } + + protected PsiReference findReferenceAtCaret() { + return TargetElementUtil.findReference(myFixture.getEditor(), myFixture.getCaretOffset()); + } + protected String getDoc() { // Note: 通过 ApplicationManager.getApplication().runReadAction(() -> {}) // 消除异常 "Read access is allowed from inside read-action" - PsiElement originalElement = myFixture.getFile() - .findElementAt(myFixture.getEditor().getCaretModel().getOffset()); + PsiElement originalElement = getElementAtCaret(); + PsiElement element = DocumentationManager.getInstance(getProject()) .findTargetElement(myFixture.getEditor(), myFixture.getFile()); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java new file mode 100644 index 000000000..70e1e3e2d --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -0,0 +1,47 @@ +package io.nop.idea.plugin.reference; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import io.nop.idea.plugin.BaseXLangPluginTestCase; + +/** + * @author flytreeleft + * @date 2025-06-22 + */ +public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { + private static final String XLANG_EXT = "xref"; + + @Override + protected String[] getXLangFileExtensions() { + return new String[] { XLANG_EXT }; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // Note: 提前将需要跳转的文件添加到 Project 中 + addVfsResourcesToProject("/nop/schema/xdef.xdef", "/nop/schema/xdsl.xdef", "/nop/schema/xmeta.xdef"); + } + + public void testGetReferencesFromXmlAttributeValue() { + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xmlns:xdef=\"/nop/schema/xdef.xdef\"", + "xmlns:xdef=\"/nop/schema/xdef.xdef\"")); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", + "xdsl:schema=\"/nop/schema/xdef.xdef\"")); + } + + /** 通过在 text 中插入 <caret> 代表光标位置 */ + private void doTest(String text) { + myFixture.configureByText("example." + XLANG_EXT, text); + + // 实际有多个引用时,将构造返回 PsiMultiReference, + // 其会按 PsiMultiReference#COMPARATOR 对引用排序得到优先引用, + // 再调用该优先引用的 #resolve() 得到 PsiElement + PsiReference ref = findReferenceAtCaret(); + assertNotNull(ref); + + PsiElement target = ref.resolve(); + assertNotNull(target); + } +} -- Gitee From 30b51f7f2d779656cd2a39eb591c735519b459b1 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Mon, 23 Jun 2025 22:57:46 +0800 Subject: [PATCH 14/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7=E5=80=BC=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 23 +- .../link/XLangGotoDeclarationHandler.java | 214 +--------------- .../plugin/reference/NopVfsFileReference.java | 97 ++++++- .../reference/XLangElementReference.java | 26 ++ .../idea/plugin/reference/XLangReference.java | 7 + .../reference/XLangReferenceProvider.java | 241 +++++++++++++++--- .../nop/idea/plugin/utils/XmlPsiHelper.java | 18 +- .../messages/NopPluginBundle.properties | 15 +- .../messages/NopPluginBundle_zh.properties | 15 +- .../reference/TestXLangReferenceProvider.java | 175 ++++++++++++- 10 files changed, 544 insertions(+), 287 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReference.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index ae1788d95..0393f0b7c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -13,6 +13,7 @@ import com.intellij.lang.annotation.Annotator; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlElement; @@ -27,6 +28,7 @@ import io.nop.core.dict.DictProvider; import io.nop.core.exceptions.ErrorMessageManager; import io.nop.idea.plugin.messages.NopPluginBundle; import io.nop.idea.plugin.reference.NopVfsFileReference; +import io.nop.idea.plugin.reference.XLangReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -56,11 +58,22 @@ public class XLangAnnotator implements Annotator { } void doAnnotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { - if (element.getReference() instanceof NopVfsFileReference) { - holder.newSilentAnnotation(HighlightSeverity.INFORMATION) - .range(element.getReference().getAbsoluteRange()) - .textAttributes(DefaultLanguageHighlighterColors.CLASS_REFERENCE) - .create(); + PsiReference ref = element.getReference(); + if (ref instanceof XLangReference) { + // TODO 需对 element 包含的多个 XLangReference 引用进行标注 + if (ref instanceof NopVfsFileReference) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(element.getReference().getAbsoluteRange()) + .textAttributes(DefaultLanguageHighlighterColors.CLASS_REFERENCE) + .create(); + } else if (ref instanceof NopVfsFileReference.NotFound vfs) { + holder.newAnnotation(HighlightSeverity.ERROR, + NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", + vfs.getPath())) + .range(element.getReference().getAbsoluteRange()) + .highlightType(ProblemHighlightType.ERROR) + .create(); + } return; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java index 243a5f52a..8cc4f047d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -7,28 +7,15 @@ */ package io.nop.idea.plugin.link; -import java.util.ArrayList; -import java.util.List; - import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandlerBase; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.resource.ProjectEnv; -import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; -import io.nop.idea.plugin.utils.XmlTagInfo; -import io.nop.xlang.xdef.IXDefNode; -import io.nop.xlang.xdef.XDefConstants; -import io.nop.xlang.xdef.XDefTypeDecl; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /*** @@ -80,61 +67,7 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { private PsiElement[] getGotoDeclarationTargetsForXmlAttributeValue( Project project, PsiElement element, int cursorOffset ) { - XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); - if (attr == null) { - return null; - } - - String attrValue = attr.getValue(); - if (StringHelper.isEmpty(attrValue)) { - return null; - } - - String attrName = attr.getName(); - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); - XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getDefAttrType(attrName) : null; - // 在无节点定义时,仅做缺省处理 - if (attrDefType == null) { - return getGotoDeclarationTargetsByPath(project, attr, attrValue); - } - - // TODO 对于声明属性,仅对其类型的定义(涉及枚举和字典)做跳转 - if (tagInfo.isDefDeclaredAttr(attrName)) { - return null; - } - - // 根据属性声明的类型,对属性值做文件/名字引用跳转处理 - PsiFile file = attr.getContainingFile(); - String stdDomain = attrDefType.getStdDomain(); - - // Note: v-path 类型采用缺省处理 - if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { - return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); - } // - else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { - return getGotoDeclarationTargetsFromXDefRef(project, attr, attrValue); - } // - else { - String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); - - if ((xdslNs + ":prototype").equals(attrName)) { - return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); - } else { - String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); - - if ((xdefNs + ":key-attr").equals(attrName)) { - return getGotoDeclarationTargetsFromKeyAttr(project, tagInfo, attrValue); - } else if ((xdefNs + ":unique-attr").equals(attrName)) { - return getGotoDeclarationTargetsFromUniqueAttr(project, tagInfo, attrValue); - } - } - } - - // 缺省:有效文件均可跳转 - // - // - // - return getGotoDeclarationTargetsByPath(project, attr, attrValue); + return null; } /** 获取可从 xml 文本中跳转的元素(文件路径、节点引用等) */ @@ -155,151 +88,6 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { return null; } - /** 获取指定路径的跳转元素(文件) */ - private PsiElement[] getGotoDeclarationTargetsByPath(Project project, @NotNull XmlElement element, String path) { - if (!StringHelper.isValidFilePath(path)) { - return null; - } - - path = XmlPsiHelper.absolutePath(path, element); - - return XmlPsiHelper.findPsiFile(project, path); - } - - /** 从 csv 文本中取光标处的跳转元素(文件) */ - private PsiElement[] getGotoDeclarationTargetsFromPathCsv( - Project project, @NotNull PsiFile file, int cursorOffset - ) { - PsiElement element = file.findElementAt(cursorOffset); - assert element != null; - - // 计算 光标所在元素 在文件中的绝对位置 - int elementStart = 0; - PsiElement parent = element; - while (parent != null && parent.getStartOffsetInParent() > 0) { - elementStart += parent.getStartOffsetInParent(); - parent = parent.getParent(); - } - - String path = extractPathFromCsv(element.getText(), cursorOffset - elementStart); - - return getGotoDeclarationTargetsByPath(project, (XmlElement) element, path); - } - - /** 从 xdef-ref 类型的属性值中获得跳转元素(文件或节点) */ - private PsiElement[] getGotoDeclarationTargetsFromXDefRef( - Project project, @NotNull XmlElement element, String attrValue - ) { - // - /nop/schema/xdef.xdef: - // - `` - // - `` - // - /nop/schema/schema/schema-node.xdef: - // `` - String target; - PsiElement[] psiFiles; - - // 含有后缀的,视为文件引用 - if (attrValue.indexOf(".") > 0) { - int hashIndex = attrValue.indexOf('#'); - String path = hashIndex > 0 ? attrValue.substring(0, hashIndex) : attrValue; - - target = hashIndex > 0 ? attrValue.substring(hashIndex + 1) : null; - psiFiles = getGotoDeclarationTargetsByPath(project, element, path); - } - // 否则,视为名字引用 - else { - target = attrValue; - // Note: 只能引用当前文件内的名字 - psiFiles = new PsiElement[] { element.getContainingFile() }; - } - - if (psiFiles == null || StringHelper.isEmpty(target)) { - return psiFiles; - } - - List result = new ArrayList<>(); - for (PsiElement psiFile : psiFiles) { - PsiTreeUtil.processElements(psiFile, el -> { - if (el instanceof XmlTag tag) { - // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 - if (target.equals(tag.getAttributeValue("xdef:name")) // - || target.equals(tag.getAttributeValue("meta:name")) // - ) { - result.add(tag); - } - } - return true; // 继续遍历 - }); - } - - return result.isEmpty() ? psiFiles : result.toArray(new PsiElement[0]); - } - - /** 从 x:prototype 的属性值中获得跳转元素(节点) */ - private PsiElement[] getGotoDeclarationTargetsFromPrototype( - Project project, XmlTagInfo tagInfo, String attrValue - ) { - // 仅从父节点中取引用到的子节点 - // io.nop.xlang.delta.DeltaMerger#mergePrototype - IXDefNode defNode = tagInfo.getDefNode(); - IXDefNode parentDefNode = tagInfo.getParentDefNode(); - - String keyAttr = parentDefNode.getXdefKeyAttr(); - if (keyAttr == null) { - keyAttr = defNode.getXdefUniqueAttr(); - } - - XmlTag parentTag = tagInfo.getTag().getParentTag(); - assert parentTag != null; - - XmlTag protoTag = XmlPsiHelper.getChildTagByAttr(parentTag, keyAttr, attrValue); - - return protoTag != null ? new PsiElement[] { protoTag } : null; - } - - /** 从 xdef:key-attr 的属性值中获得跳转元素(节点属性) */ - private PsiElement[] getGotoDeclarationTargetsFromKeyAttr( - Project project, XmlTagInfo tagInfo, String attrValue - ) { - return XmlPsiHelper.getAttrsFromChildTag(tagInfo.getTag(), attrValue); - } - - /** 从 xdef:unique-attr 的属性值中获得跳转元素(节点属性) */ - private PsiElement[] getGotoDeclarationTargetsFromUniqueAttr( - Project project, XmlTagInfo tagInfo, String attrValue - ) { - // 仅从当前节点中取引用到的属性 - XmlTag tag = tagInfo.getTag(); - XmlAttribute attr = tag.getAttribute(attrValue); - - return attr != null ? new PsiElement[] { attr } : null; - } - - /** 从 csv 中提取指定偏移位置所在的文件路径 */ - private String extractPathFromCsv(String csv, int offset) { - int start = offset; - int end = offset; - - while (start > 0) { - char ch = csv.charAt(start - 1); - if (ch != ',' && !Character.isWhitespace(ch)) { - start -= 1; - } else { - break; - } - } - while (end < csv.length()) { - char ch = csv.charAt(end); - if (ch != ',' && !Character.isWhitespace(ch)) { - end += 1; - } else { - break; - } - } - - return csv.substring(start, end); - } - private boolean isCustomTag(String tagName) { int pos = tagName.indexOf(':'); if (pos <= 0) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java index ad19abeae..68461e466 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java @@ -8,10 +8,12 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.util.ArrayUtil; +import io.nop.idea.plugin.utils.XmlPsiHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,8 +23,10 @@ import org.jetbrains.annotations.Nullable; * @author flytreeleft * @date 2025-06-22 */ -public class NopVfsFileReference extends PsiReferenceBase { +public class NopVfsFileReference extends PsiReferenceBase implements XLangReference { private final PsiFile file; + /** 文件内的定位锚点 */ + private final Anchor anchor; /** * @param refElement @@ -32,26 +36,46 @@ public class NopVfsFileReference extends PsiReferenceBase { * 指定对应引用的关联文本所在的文本范围,该范围相对于该元素,从 0 开始计算。 * 相关处理逻辑见 {@link com.intellij.psi.impl.SharedPsiElementImplUtil#addReferences SharedPsiElementImplUtil#addReferences}) */ - public NopVfsFileReference(@NotNull XmlElement refElement, TextRange textRange, @NotNull PsiFile file) { + public NopVfsFileReference( + @NotNull XmlElement refElement, TextRange textRange, // + @NotNull PsiFile file, Anchor anchor + ) { super(refElement, textRange, // 不采用延迟解析模式,以确保当解析到有其他相关引用时,其能够被 PsiMultiReference 作为最优引用 false); this.file = file; + this.anchor = anchor; } /** 得到具体的引用对象(文件、文件行、文件内某个元素等) */ @Override public @Nullable PsiElement resolve() { - // TODO 在光标显示引用信息时,参考 class 文档样式显示 xdef/dsl 的 vfs 路径及其文档:JavaDocumentationProvider List results = new ArrayList<>(); - PsiTreeUtil.processElements(this.file, el -> { - if (el instanceof XmlTag tag) { - results.add(tag); - // 仅取根节点 - return false; + + if (anchor instanceof PosAnchor pos) { + PsiElement el = XmlPsiHelper.getPsiElementAt(file, pos.line, pos.column); + if (el != null) { + results.add(el); } - return true; // 继续遍历 - }); + } else { + PsiTreeUtil.processElements(file, element -> { + if (!(element instanceof XmlElement el)) { + return true; // 跳过非 xml 元素 + } + + if (anchor == null) { + if (el instanceof XmlTag) { + results.add(el); + } + } else if (anchor instanceof RefAnchor ref) { + if (ref.match(el)) { + results.add(el); + } + } + // 仅取第一个匹配到的元素,否则,继续遍历 + return results.isEmpty(); + }); + } return results.isEmpty() ? this.file : results.get(0); } @@ -66,4 +90,57 @@ public class NopVfsFileReference extends PsiReferenceBase { public boolean isReferenceTo(@NotNull PsiElement target) { return target instanceof PsiFile && ((PsiFile) target).getVirtualFile().getPath().endsWith("/_vfs" + file); } + + public static class NotFound extends PsiReferenceBase implements XLangReference { + private final String path; + + public NotFound(@NotNull XmlElement refElement, TextRange textRange, String path) { + super(refElement, textRange, false); + this.path = path; + } + + public String getPath() { + return path; + } + + @Override + public @Nullable PsiElement resolve() { + return null; + } + + @Override + public @NotNull Object @NotNull [] getVariants() { + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + @Override + public boolean isReferenceTo(@NotNull PsiElement target) { + return false; + } + } + + public interface Anchor {} + + public record PosAnchor(int line, int column) implements Anchor {} + + public record RefAnchor(String value) implements Anchor { + + public boolean match(XmlElement element) { + if (value == null) { + // 若未引用名字,则匹配 xml 根节点 + return element instanceof XmlTag; + } // + else if (element instanceof XmlAttribute attr) { + String attrName = attr.getName(); + String attrValue = attr.getValue(); + + // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 + return ("xdef:name".equals(attrName) // + || "meta:name".equals(attrName) // + ) && value.equals(attrValue); + } + + return false; + } + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java new file mode 100644 index 000000000..f409679d6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java @@ -0,0 +1,26 @@ +package io.nop.idea.plugin.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.xml.XmlElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author flytreeleft + * @date 2025-06-23 + */ +public class XLangElementReference extends PsiReferenceBase implements XLangReference { + private final XmlElement target; + + public XLangElementReference(@NotNull XmlElement element, TextRange rangeInElement, XmlElement target) { + super(element, rangeInElement); + this.target = target; + } + + @Override + public @Nullable PsiElement resolve() { + return target; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReference.java new file mode 100644 index 000000000..35cf1206b --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReference.java @@ -0,0 +1,7 @@ +package io.nop.idea.plugin.reference; + +/** + * @author flytreeleft + * @date 2025-06-23 + */ +public interface XLangReference {} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java index 44890b8f0..4a5dc6ced 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -1,5 +1,11 @@ package io.nop.idea.plugin.reference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; @@ -13,11 +19,15 @@ import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.util.ProcessingContext; +import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.utils.XmlTagInfo; +import io.nop.xlang.xdef.IXDefAttribute; +import io.nop.xlang.xdef.IXDefNode; +import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import org.jetbrains.annotations.NotNull; @@ -87,16 +97,38 @@ public class XLangReferenceProvider extends PsiReferenceProvider { }); } + /** 获取 xml 标签对应的引用(节点定义、xpl 函数定义等) */ private PsiReference @NotNull [] getReferencesFromXmlTag(Project project, XmlElement refElement, XmlTag tag) { + // TODO xpl 函数的引用 + return PsiReference.EMPTY_ARRAY; } + /** 获取 xml 属性名对应的引用(属性定义) */ private PsiReference @NotNull [] getReferencesFromXmlAttribute( Project project, XmlElement refElement, XmlAttribute attr ) { - return PsiReference.EMPTY_ARRAY; + String attrName = attr.getName(); + XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); + IXDefAttribute attrDef = tagInfo != null ? tagInfo.getDefAttr(attrName) : null; + + if (attrDef == null) { + return PsiReference.EMPTY_ARRAY; + } + + SourceLocation loc = attrDef.getLocation(); + String path = loc.getPath(); + + TextRange textRange = new TextRange(0, attrName.length()); + + return getReferencesByVfsPath(project, + refElement, + path, + textRange, + new NopVfsFileReference.PosAnchor(loc.getLine(), loc.getPos())); } + /** 获取 xml 属性值对应的引用(文件、节点、属性类型等) */ private PsiReference @NotNull [] getReferencesFromXmlAttributeValue( Project project, XmlAttributeValue refElement, XmlAttribute attr ) { @@ -112,7 +144,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getDefAttrType(attrName) : null; // 在无节点定义时,仅做缺省处理 if (attrDefType == null) { - return getReferencesByPath(project, refElement, attrValue); + return getReferencesByVfsPath(project, refElement, attrValue); } // TODO 对于声明属性,仅对其类型的定义(涉及枚举和字典)做跳转 @@ -121,37 +153,36 @@ public class XLangReferenceProvider extends PsiReferenceProvider { } // 根据属性声明的类型,对属性值做文件/名字引用跳转处理 - PsiFile file = attr.getContainingFile(); String stdDomain = attrDefType.getStdDomain(); -// // Note: v-path 类型采用缺省处理 -// if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { -// return getGotoDeclarationTargetsFromPathCsv(project, file, cursorOffset); -// } // -// else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { -// return getGotoDeclarationTargetsFromXDefRef(project, attr, attrValue); -// } // -// else { -// String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); -// -// if ((xdslNs + ":prototype").equals(attrName)) { -// return getGotoDeclarationTargetsFromPrototype(project, tagInfo, attrValue); -// } else { -// String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); -// -// if ((xdefNs + ":key-attr").equals(attrName)) { -// return getGotoDeclarationTargetsFromKeyAttr(project, tagInfo, attrValue); -// } else if ((xdefNs + ":unique-attr").equals(attrName)) { -// return getGotoDeclarationTargetsFromUniqueAttr(project, tagInfo, attrValue); -// } -// } -// } + // Note: v-path 类型采用缺省处理 + if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { + return getReferencesFromVfsPathCsv(project, refElement, attrValue); + } // + else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { + return getReferencesFromXDefRef(project, refElement, attrValue); + } // + else { + String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); + + if ((xdslNs + ":prototype").equals(attrName)) { + return getReferencesFromPrototype(project, refElement, tagInfo, attrValue); + } else { + String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); + + if ((xdefNs + ":key-attr").equals(attrName)) { + return getReferencesFromKeyAttr(project, refElement, tagInfo, attrValue); + } else if ((xdefNs + ":unique-attr").equals(attrName)) { + return getReferencesFromUniqueAttr(project, refElement, tagInfo, attrValue); + } + } + } // 缺省引用识别 // // // - return getReferencesByPath(project, refElement, attrValue); + return getReferencesByVfsPath(project, refElement, attrValue); } private PsiReference[] getReferencesFromXmlText(Project project, XmlElement refElement, XmlTag tag) { @@ -159,20 +190,166 @@ public class XLangReferenceProvider extends PsiReferenceProvider { } /** 获取指定路径的引用(文件) */ - private PsiReference[] getReferencesByPath(Project project, @NotNull XmlAttributeValue refElement, String path) { - if (!StringHelper.isValidFilePath(path)) { + private PsiReference[] getReferencesByVfsPath(Project project, XmlAttributeValue refElement, String path) { + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange textRange = new TextRange(1, path.length() + 1); + + return getReferencesByVfsPath(project, refElement, path, textRange, null); + } + + /** 从 csv 文本中获取引用 */ + private PsiReference[] getReferencesFromVfsPathCsv(Project project, XmlAttributeValue refElement, String csv) { + // Note: XmlAttributeValue 的文本范围是包含引号的 + Map rangePathMap = extractPathsFromCsv(csv); + + List list = new ArrayList<>(rangePathMap.size()); + rangePathMap.forEach((textRange, path) -> { + PsiReference[] refs = getReferencesByVfsPath(project, refElement, path, textRange.shiftRight(1), null); + + list.addAll(Arrays.stream(refs).toList()); + }); + + return list.toArray(PsiReference[]::new); + } + + /** 从 xdef-ref 类型的属性值中获取引用 */ + private PsiReference[] getReferencesFromXDefRef( + Project project, XmlAttributeValue refElement, String xdefRefValue + ) { + // - /nop/schema/xdef.xdef: + // - `` + // - `` + // - /nop/schema/schema/schema-node.xdef: + // `` + + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange textRange = new TextRange(1, xdefRefValue.length() + 1); + + // 含有后缀的,视为文件引用 + if (xdefRefValue.indexOf(".") > 0) { + int hashIndex = xdefRefValue.indexOf('#'); + + String path = hashIndex > 0 ? xdefRefValue.substring(0, hashIndex) : xdefRefValue; + String ref = hashIndex > 0 ? xdefRefValue.substring(hashIndex + 1) : null; + NopVfsFileReference.Anchor anchor = new NopVfsFileReference.RefAnchor(ref); + + return getReferencesByVfsPath(project, refElement, path, textRange, anchor); + } + // 否则,视为名字引用 + else { + String ref = xdefRefValue; + NopVfsFileReference.Anchor anchor = new NopVfsFileReference.RefAnchor(ref); + // Note: 只能引用当前文件(不一定是 vfs)内的名字 + PsiFile file = refElement.getContainingFile(); + + return new PsiReference[] { + new NopVfsFileReference(refElement, textRange, file, anchor) + }; + } + } + + /** 从 x:prototype 的属性值中获取引用 */ + private PsiReference[] getReferencesFromPrototype( + Project project, XmlAttributeValue refElement, XmlTagInfo tagInfo, String attrValue + ) { + // 仅从父节点中取引用到的子节点 + // io.nop.xlang.delta.DeltaMerger#mergePrototype + IXDefNode defNode = tagInfo.getDefNode(); + IXDefNode parentDefNode = tagInfo.getParentDefNode(); + + String keyAttr = parentDefNode.getXdefKeyAttr(); + if (keyAttr == null) { + keyAttr = defNode.getXdefUniqueAttr(); + } + + XmlTag parentTag = tagInfo.getTag().getParentTag(); + if (parentTag == null) { + return PsiReference.EMPTY_ARRAY; + } + + XmlTag protoTag = XmlPsiHelper.getChildTagByAttr(parentTag, keyAttr, attrValue); + if (protoTag == null) { return PsiReference.EMPTY_ARRAY; } // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, path.length() + 1); - String absPath = XmlPsiHelper.absolutePath(path, refElement); + TextRange textRange = new TextRange(1, attrValue.length() + 1); + + return new PsiReference[] { new XLangElementReference(refElement, textRange, protoTag) }; + } - return XmlPsiHelper.findPsiFileList(project, absPath) + /** 从 xdef:key-attr 的属性值中获取引用 */ + private PsiReference[] getReferencesFromKeyAttr( + Project project, XmlAttributeValue refElement, XmlTagInfo tagInfo, String attrValue + ) { + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange textRange = new TextRange(1, attrValue.length() + 1); + + return XmlPsiHelper.getAttrsFromChildTag(tagInfo.getTag(), attrValue) .stream() - .map((file) -> new NopVfsFileReference(refElement, textRange, file)) + .map((attr) -> new XLangElementReference(refElement, textRange, attr)) .toArray(PsiReference[]::new); } + + /** 从 xdef:unique-attr 的属性值中获取引用 */ + private PsiReference[] getReferencesFromUniqueAttr( + Project project, XmlAttributeValue refElement, XmlTagInfo tagInfo, String attrValue + ) { + // 仅从当前节点中取引用到的属性 + XmlTag tag = tagInfo.getTag(); + XmlAttribute attr = tag.getAttribute(attrValue); + if (attr == null) { + return PsiReference.EMPTY_ARRAY; + } + + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange textRange = new TextRange(1, attrValue.length() + 1); + + return new PsiReference[] { new XLangElementReference(refElement, textRange, attr) }; + } + + private PsiReference[] getReferencesByVfsPath( + Project project, XmlElement refElement, // + String path, TextRange textRange, NopVfsFileReference.Anchor anchor + ) { + if (!StringHelper.isValidFilePath(path) || path.indexOf('.') <= 0) { + return PsiReference.EMPTY_ARRAY; + } + + String absPath = XmlPsiHelper.absolutePath(path, refElement); + + PsiReference[] refs = XmlPsiHelper.findPsiFileList(project, absPath) + .stream() + .map((file) -> new NopVfsFileReference(refElement, textRange, file, anchor)) + .toArray(PsiReference[]::new); + + return refs.length > 0 // + ? refs // + : new PsiReference[] { new NopVfsFileReference.NotFound(refElement, textRange, path) }; + } + + private Map extractPathsFromCsv(String csv) { + Map rangePathMap = new HashMap<>(); + + int offset = 0; + for (int i = 0; i < csv.length(); i++) { + char ch = csv.charAt(i); + if (ch != ',') { + continue; + } + + String path = csv.substring(offset, i); + rangePathMap.put(new TextRange(offset, i), path); + + offset = i + 1; + } + + if (offset < csv.length()) { + String path = csv.substring(offset); + rangePathMap.put(new TextRange(offset, csv.length()), path); + } + return rangePathMap; + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index e31f4c547..5d46e3358 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -172,17 +172,23 @@ public class XmlPsiHelper { return ProjectFileHelper.getNopVfsPath(vf); } + /** 获取指定行列的 {@link PsiElement 元素} */ + public static PsiElement getPsiElementAt(PsiFile psiFile, int line, int column) { + Document document = psiFile.getViewProvider().getDocument(); + assert document != null; + + int offset = document.getLineStartOffset(line - 1) + column - 1; + return psiFile.findElementAt(offset); + } + public static SourceLocation getLocation(PsiElement element) { return getLocation(element, element.getTextOffset(), element.getTextLength()); } public static SourceLocation getValueLocation(XmlTag element) { - if (element.getValue() == null) { - return null; - } - TextRange range = element.getValue().getTextRange(); int offset = range.getStartOffset(); + return getLocation(element, offset, range.getLength()); } @@ -294,7 +300,7 @@ public class XmlPsiHelper { } /** 根据查找子节点上指定名字的属性 */ - public static XmlAttribute[] getAttrsFromChildTag(XmlTag tag, String attrName) { + public static List getAttrsFromChildTag(XmlTag tag, String attrName) { List attrs = new ArrayList<>(); for (PsiElement element : tag.getChildren()) { @@ -307,7 +313,7 @@ public class XmlPsiHelper { attrs.add(attr); } } - return attrs.isEmpty() ? null : attrs.toArray(new XmlAttribute[0]); + return attrs; } public static XmlTag getXmlTag(PsiElement element) { diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties index 8fb787325..bc0af2e95 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties @@ -1,10 +1,11 @@ line.breakpoints.tab.title=XLang line breakpoints -xlang.annotation.invalid.tag=Invalid tag name: {0} -xlang.annotation.invalid.attr.name=Invalid attr name: {0} -xlang.annotation.invalid.attr.value=Invalid attr value: {0} -xlang.annotation.attr.not-allow-empty=Attr {0} not allow empty -xlang.annotation.value.not-allow-empty=Tag {0} value not allow empty -xlang.annotation.tag.not-allow-value=Tag {0} not allow value -xlang.annotation.tag.not-allow-child=Tag {0} not allow child +xlang.annotation.invalid.tag=Invalid tag name: ''{0}'' +xlang.annotation.invalid.attr.name=Invalid attr name: ''{0}'' +xlang.annotation.invalid.attr.value=Invalid attr value: ''{0}'' +xlang.annotation.attr.not-allow-empty=Attr ''{0}'' not allow empty +xlang.annotation.value.not-allow-empty=Tag ''{0}'' value not allow empty +xlang.annotation.tag.not-allow-value=Tag ''{0}'' not allow value +xlang.annotation.tag.not-allow-child=Tag ''{0}'' not allow child +xlang.annotation.reference.vfs-file-not-found=The referenced vfs file ''{0}'' doesn''t exist yet xlang.doc.markdown.link-title='{'Link'}'{0} xlang.doc.markdown.image-title='{'Image'}'{0} diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties index e06849a87..0457b255f 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties @@ -1,10 +1,11 @@ line.breakpoints.tab.title=XLang line breakpoints -xlang.annotation.invalid.tag=\u975E\u6CD5\u7684\u6807\u7B7E\u540D: {0} -xlang.annotation.invalid.attr.name=\u975E\u6CD5\u7684\u5C5E\u6027\u540D: {0} -xlang.annotation.invalid.attr.value=\u975E\u6CD5\u7684\u5C5E\u6027\u503C: {0} -xlang.annotation.attr.not-allow-empty=\u5C5E\u6027{0}\u4E0D\u5141\u8BB8\u4E3A\u7A7A -xlang.annotation.value.not-allow-empty=\u6807\u7B7E{0}\u7684\u503C\u4E0D\u5141\u8BB8\u4E3A\u7A7A -xlang.annotation.tag.not-allow-value=\u6807\u7B7E{0}\u4E0D\u5141\u8BB8\u5185\u5BB9\u8282\u70B9 -xlang.annotation.tag.not-allow-child=\u6807\u7B7E{0}\u4E0D\u5141\u8BB8\u5B50\u8282\u70B9 +xlang.annotation.invalid.tag=\u975E\u6CD5\u7684\u6807\u7B7E\u540D: ''{0}'' +xlang.annotation.invalid.attr.name=\u975E\u6CD5\u7684\u5C5E\u6027\u540D: ''{0}'' +xlang.annotation.invalid.attr.value=\u975E\u6CD5\u7684\u5C5E\u6027\u503C: ''{0}'' +xlang.annotation.attr.not-allow-empty=\u5C5E\u6027 ''{0}'' \u4E0D\u5141\u8BB8\u4E3A\u7A7A +xlang.annotation.value.not-allow-empty=\u6807\u7B7E ''{0}'' \u7684\u503C\u4E0D\u5141\u8BB8\u4E3A\u7A7A +xlang.annotation.tag.not-allow-value=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5185\u5BB9\u8282\u70B9 +xlang.annotation.tag.not-allow-child=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5B50\u8282\u70B9 +xlang.annotation.reference.vfs-file-not-found=\u5F15\u7528\u7684 vfs \u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 xlang.doc.markdown.link-title='{'\u94FE\u63A5'}'{0} xlang.doc.markdown.image-title='{'\u56FE\u7247'}'{0} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index 70e1e3e2d..e74b31ddc 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -2,7 +2,13 @@ package io.nop.idea.plugin.reference; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlElement; +import com.intellij.psi.xml.XmlTag; import io.nop.idea.plugin.BaseXLangPluginTestCase; +import io.nop.idea.plugin.utils.XmlPsiHelper; /** * @author flytreeleft @@ -20,28 +26,183 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { protected void setUp() throws Exception { super.setUp(); - // Note: 提前将需要跳转的文件添加到 Project 中 - addVfsResourcesToProject("/nop/schema/xdef.xdef", "/nop/schema/xdsl.xdef", "/nop/schema/xmeta.xdef"); + // Note: 提前将被引用的文件添加到 Project 中 + addVfsResourcesToProject("/nop/schema/xdef.xdef", + "/nop/schema/xdsl.xdef", + "/nop/schema/xmeta.xdef", + "/nop/schema/xui/xview.xdef", + "/nop/schema/xui/store.xdef", + "/nop/core/xlib/meta-gen.xlib", + "/nop/schema/schema/obj-schema.xdef", + "/dict/test/doc/child-type.dict.yaml", + "/test/link/a.xmeta", + "/test/link/b.xmeta", + "/test/link/a.xlib", + "/test/link/default.xform", + "/test/link/test-filter.xdef"); } public void testGetReferencesFromXmlAttributeValue() { - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xmlns:xdef=\"/nop/schema/xdef.xdef\"", - "xmlns:xdef=\"/nop/schema/xdef.xdef\"")); + // 对 v-path-list 列表元素的引用 + doTest(""" + + """, "/test/link/a.xmeta"); + doTest(""" + + """, "/test/link/b.xmeta"); + + // 对 xdef-ref 类型属性的引用 + // - 在 *.xdef 中引用内部名字 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", + "meta:ref=\"XDefNode\""), "#XDefNode"); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), + "#DslNode"); + // - 在 *.xdef 中引用外部文件 + doTest(""" + + """, "/nop/schema/xdsl.xdef"); + doTest(""" + + """, "/nop/schema/schema/obj-schema.xdef"); + // - 在 *.xmeta 中引用外部文件中的节点 + doTest(""" + + """, "/test/link/test-filter.xdef#FilterCondition"); + + // 对 x:prototype 属性值的引用 + doTest(readVfsResource("/test/link/user.view.xml").replace("x:prototype=\"list\"", + "x:prototype=\"list\""), "grid#list"); + doTest(readVfsResource("/test/link/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), + "Get"); + + // 对唯一键的引用 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", + "meta:unique-attr=\"name\""), "xdef:prop#name"); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"xdef:name\"", + "meta:unique-attr=\"xdef:name\""), + "xdef:define#xdef:name"); + doTest(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"id\"", "xdef:key-attr=\"id\""), + "selection#id"); + + // 缺省:对有效文件的引用 + doTest(""" + + """, "/test/link/a.xlib"); + doTest(""" + + """, "/test/link/a.xlib"); + doTest(""" + + """, "/test/link/default.xform"); + // - x:schema=v-path + doTest(""" + + """, "/nop/schema/xmeta.xdef"); + // - xdef:default-extends=v-path + doTest(""" + + """, "/test/link/default.xform"); + // - xpl:lib=v-path + doTest(""" + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); + doTest(""" + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); + doTest(""" + + + + + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); + // - x:schema=v-path + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", + "x:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", - "xdsl:schema=\"/nop/schema/xdef.xdef\"")); + "xdsl:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + +// // TODO 声明属性将引用属性的类型定义 +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", +// "xdef:ref=\"xdef-ref\""), ""); +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("-name\""), ""); +// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", "x:schema=\"v-path\""), +// ""); } /** 通过在 text 中插入 <caret> 代表光标位置 */ - private void doTest(String text) { + private void doTest(String text, String expected) { myFixture.configureByText("example." + XLANG_EXT, text); // 实际有多个引用时,将构造返回 PsiMultiReference, // 其会按 PsiMultiReference#COMPARATOR 对引用排序得到优先引用, // 再调用该优先引用的 #resolve() 得到 PsiElement PsiReference ref = findReferenceAtCaret(); - assertNotNull(ref); + if (!(ref instanceof XLangReference)) { + assertInstanceOf(ref, PsiMultiReference.class); + + ref = ((PsiMultiReference) ref).getReferences()[0]; + assertInstanceOf(ref, XLangReference.class); + } PsiElement target = ref.resolve(); assertNotNull(target); + + if (ref instanceof NopVfsFileReference) { + // Note: 可能不是 vfs 文件 + String vfsPath = XmlPsiHelper.getNopVfsPath(target); + String anchor = target instanceof XmlAttribute attr ? attr.getValue() : null; + + assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); + } else if (ref instanceof XLangElementReference) { + assertInstanceOf(target, XmlElement.class); + + if (target instanceof XmlTag tag) { + String id = tag.getAttributeValue("id"); + + assertEquals(expected, tag.getName() + (id != null ? "#" + id : "")); + } else if (target instanceof XmlAttribute attr) { + XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); + + assertEquals(expected, tag.getName() + "#" + attr.getName()); + } else { + fail("Unknown target " + target.getClass()); + } + } } } -- Gitee From fbba17a0f4e8838e86eabf3c12142dd69ea4e346 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 24 Jun 2025 16:11:22 +0800 Subject: [PATCH 15/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7=E5=80=BC=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB=EF=BC=8C=E7=BC=BA=E7=9C=81?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AF=B9=20*.xdef=20=E7=9A=84=E5=BC=95?= =?UTF-8?q?=E7=94=A8=EF=BC=8C=E5=B9=B6=E5=A2=9E=E5=8A=A0=E5=AF=B9=E5=90=8D?= =?UTF-8?q?=E5=AD=97=E5=BC=95=E7=94=A8=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 24 +- .../link/XLangGotoDeclarationHandler.java | 4 +- .../reference/XLangElementReference.java | 2 + .../reference/XLangNotFoundReference.java | 43 ++++ .../reference/XLangReferenceProvider.java | 223 ++++++++++++------ ...erence.java => XLangVfsFileReference.java} | 87 +------ .../nop/idea/plugin/utils/XmlPsiHelper.java | 64 +++-- .../io/nop/idea/plugin/utils/XmlTagInfo.java | 6 + .../messages/NopPluginBundle.properties | 9 +- .../messages/NopPluginBundle_zh.properties | 7 + .../reference/TestXLangReferenceProvider.java | 182 ++++++++++---- 11 files changed, 412 insertions(+), 239 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/{NopVfsFileReference.java => XLangVfsFileReference.java} (47%) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index 0393f0b7c..b40e7ef0b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -27,8 +27,9 @@ import io.nop.commons.util.StringHelper; import io.nop.core.dict.DictProvider; import io.nop.core.exceptions.ErrorMessageManager; import io.nop.idea.plugin.messages.NopPluginBundle; -import io.nop.idea.plugin.reference.NopVfsFileReference; -import io.nop.idea.plugin.reference.XLangReference; +import io.nop.idea.plugin.reference.XLangVfsFileReference; +import io.nop.idea.plugin.reference.XLangElementReference; +import io.nop.idea.plugin.reference.XLangNotFoundReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -58,23 +59,20 @@ public class XLangAnnotator implements Annotator { } void doAnnotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { - PsiReference ref = element.getReference(); - if (ref instanceof XLangReference) { - // TODO 需对 element 包含的多个 XLangReference 引用进行标注 - if (ref instanceof NopVfsFileReference) { + for (PsiReference reference : element.getReferences()) { + if (reference instanceof XLangVfsFileReference // + || reference instanceof XLangElementReference // + ) { holder.newSilentAnnotation(HighlightSeverity.INFORMATION) - .range(element.getReference().getAbsoluteRange()) + .range(reference.getAbsoluteRange()) .textAttributes(DefaultLanguageHighlighterColors.CLASS_REFERENCE) .create(); - } else if (ref instanceof NopVfsFileReference.NotFound vfs) { - holder.newAnnotation(HighlightSeverity.ERROR, - NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", - vfs.getPath())) - .range(element.getReference().getAbsoluteRange()) + } else if (reference instanceof XLangNotFoundReference ref) { + holder.newAnnotation(HighlightSeverity.ERROR, ref.getMessage()) + .range(ref.getAbsoluteRange()) .highlightType(ProblemHighlightType.ERROR) .create(); } - return; } if (element instanceof XmlTag) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java index 8cc4f047d..aa3048567 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java @@ -80,9 +80,9 @@ public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { if ((text.indexOf('.') > 0 || text.indexOf('/') > 0) // && StringHelper.isValidFilePath(text) // ) { - String path = XmlPsiHelper.absolutePath(text, parent); + String path = XmlPsiHelper.getNopVfsAbsolutePath(text, parent); - return XmlPsiHelper.findPsiFile(project, path); + return XmlPsiHelper.findPsiFiles(project, path); } return null; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java index f409679d6..15a29bd6e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java @@ -8,6 +8,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** + * 对 XLang 节点或属性等元素的引用 + * * @author flytreeleft * @date 2025-06-23 */ diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java new file mode 100644 index 000000000..b747ab6a4 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java @@ -0,0 +1,43 @@ +package io.nop.idea.plugin.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.xml.XmlElement; +import com.intellij.util.ArrayUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * XLang 引用不存在 + * + * @author flytreeleft + * @date 2025-06-24 + */ +public class XLangNotFoundReference extends PsiReferenceBase implements XLangReference { + private final String message; + + public XLangNotFoundReference(@NotNull XmlElement refElement, TextRange textRange, String message) { + super(refElement, textRange, false); + this.message = message; + } + + public String getMessage() { + return message; + } + + @Override + public @Nullable PsiElement resolve() { + return null; + } + + @Override + public @NotNull Object @NotNull [] getVariants() { + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + @Override + public boolean isReferenceTo(@NotNull PsiElement target) { + return false; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java index 4a5dc6ced..6b301f0d1 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -2,6 +2,7 @@ package io.nop.idea.plugin.reference; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -21,6 +22,7 @@ import com.intellij.psi.xml.XmlTag; import com.intellij.util.ProcessingContext; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.messages.NopPluginBundle; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -69,12 +71,12 @@ public class XLangReferenceProvider extends PsiReferenceProvider { XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); if (attr != null) { - return getReferencesFromXmlAttribute(project, (XmlElement) element, attr); + return getReferencesFromXmlAttribute((XmlElement) element, attr); } else { XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); if (tag != null) { - return getReferencesFromXmlTag(project, (XmlElement) element, tag); + return getReferencesFromXmlTag((XmlElement) element, tag); } } } // @@ -82,14 +84,14 @@ public class XLangReferenceProvider extends PsiReferenceProvider { XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); if (attr != null) { - return getReferencesFromXmlAttributeValue(project, (XmlAttributeValue) element, attr); + return getReferencesFromXmlAttributeValue((XmlAttributeValue) element, attr); } } // else if (isElementType(element, XmlElementType.XML_TEXT)) { XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); if (tag != null) { - return getReferencesFromXmlText(project, (XmlElement) element, tag); + return getReferencesFromXmlText((XmlElement) element, tag); } } @@ -98,16 +100,14 @@ public class XLangReferenceProvider extends PsiReferenceProvider { } /** 获取 xml 标签对应的引用(节点定义、xpl 函数定义等) */ - private PsiReference @NotNull [] getReferencesFromXmlTag(Project project, XmlElement refElement, XmlTag tag) { + private PsiReference @NotNull [] getReferencesFromXmlTag(XmlElement refElement, XmlTag tag) { // TODO xpl 函数的引用 return PsiReference.EMPTY_ARRAY; } /** 获取 xml 属性名对应的引用(属性定义) */ - private PsiReference @NotNull [] getReferencesFromXmlAttribute( - Project project, XmlElement refElement, XmlAttribute attr - ) { + private PsiReference @NotNull [] getReferencesFromXmlAttribute(XmlElement refElement, XmlAttribute attr) { String attrName = attr.getName(); XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); IXDefAttribute attrDef = tagInfo != null ? tagInfo.getDefAttr(attrName) : null; @@ -121,16 +121,15 @@ public class XLangReferenceProvider extends PsiReferenceProvider { TextRange textRange = new TextRange(0, attrName.length()); - return getReferencesByVfsPath(project, - refElement, + return getReferencesByVfsPath(refElement, path, textRange, - new NopVfsFileReference.PosAnchor(loc.getLine(), loc.getPos())); + new XLangVfsFileReference.PosAnchor(loc.getLine(), loc.getPos())); } /** 获取 xml 属性值对应的引用(文件、节点、属性类型等) */ private PsiReference @NotNull [] getReferencesFromXmlAttributeValue( - Project project, XmlAttributeValue refElement, XmlAttribute attr + XmlAttributeValue refElement, XmlAttribute attr ) { // Note: XmlAttributeValue#getValue 的结果包含引号 String attrValue = attr.getValue(); @@ -142,9 +141,9 @@ public class XLangReferenceProvider extends PsiReferenceProvider { XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getDefAttrType(attrName) : null; - // 在无节点定义时,仅做缺省处理 + // 在无节点定义时,则做默认识别 if (attrDefType == null) { - return getReferencesByVfsPath(project, refElement, attrValue); + return getReferencesByDefault(refElement, attrValue); } // TODO 对于声明属性,仅对其类型的定义(涉及枚举和字典)做跳转 @@ -156,55 +155,72 @@ public class XLangReferenceProvider extends PsiReferenceProvider { String stdDomain = attrDefType.getStdDomain(); // Note: v-path 类型采用缺省处理 - if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { - return getReferencesFromVfsPathCsv(project, refElement, attrValue); + if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain) // + || XDefConstants.STD_DOMAIN_NAME_OR_V_PATH.equals(stdDomain) // + ) { + return getReferencesByVfsPath(refElement, attrValue); + } // + else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { + return getReferencesFromVfsPathCsv(refElement, attrValue); } // else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { - return getReferencesFromXDefRef(project, refElement, attrValue); + return getReferencesFromXDefRef(refElement, attrValue); } // else { String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); if ((xdslNs + ":prototype").equals(attrName)) { - return getReferencesFromPrototype(project, refElement, tagInfo, attrValue); + return getReferencesFromPrototype(refElement, attrValue, tagInfo); } else { String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); if ((xdefNs + ":key-attr").equals(attrName)) { - return getReferencesFromKeyAttr(project, refElement, tagInfo, attrValue); + return getReferencesFromKeyAttr(refElement, attrValue, tagInfo); } else if ((xdefNs + ":unique-attr").equals(attrName)) { - return getReferencesFromUniqueAttr(project, refElement, tagInfo, attrValue); + return getReferencesFromUniqueAttr(refElement, attrValue, tagInfo); } } } - // 缺省引用识别 + // TODO 其他引用识别 // // - // - return getReferencesByVfsPath(project, refElement, attrValue); + + return getReferencesByDefault(refElement, attrValue); } - private PsiReference[] getReferencesFromXmlText(Project project, XmlElement refElement, XmlTag tag) { + private PsiReference[] getReferencesFromXmlText(XmlElement refElement, XmlTag tag) { return PsiReference.EMPTY_ARRAY; } + /** 对文本做默认的引用识别 */ + private PsiReference[] getReferencesByDefault(XmlAttributeValue attrValueElement, String attrValue) { + if (!attrValue.endsWith(".xdef")) { + return PsiReference.EMPTY_ARRAY; + } + + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange textRange = new TextRange(1, attrValue.length() + 1); + + return getReferencesByVfsPath(attrValueElement, attrValue, textRange, null); + } + /** 获取指定路径的引用(文件) */ - private PsiReference[] getReferencesByVfsPath(Project project, XmlAttributeValue refElement, String path) { + private PsiReference[] getReferencesByVfsPath(XmlAttributeValue attrValueElement, String attrValue) { // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, path.length() + 1); + TextRange textRange = new TextRange(1, attrValue.length() + 1); - return getReferencesByVfsPath(project, refElement, path, textRange, null); + return getReferencesByVfsPath(attrValueElement, attrValue, textRange, null); } /** 从 csv 文本中获取引用 */ - private PsiReference[] getReferencesFromVfsPathCsv(Project project, XmlAttributeValue refElement, String csv) { + private PsiReference[] getReferencesFromVfsPathCsv(XmlAttributeValue attrValueElement, String attrValue) { // Note: XmlAttributeValue 的文本范围是包含引号的 - Map rangePathMap = extractPathsFromCsv(csv); + Map rangePathMap = extractPathsFromCsv(attrValue); List list = new ArrayList<>(rangePathMap.size()); rangePathMap.forEach((textRange, path) -> { - PsiReference[] refs = getReferencesByVfsPath(project, refElement, path, textRange.shiftRight(1), null); + PsiReference[] refs = getReferencesByVfsPath(attrValueElement, path, textRange.shiftRight(1), null); list.addAll(Arrays.stream(refs).toList()); }); @@ -213,9 +229,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { } /** 从 xdef-ref 类型的属性值中获取引用 */ - private PsiReference[] getReferencesFromXDefRef( - Project project, XmlAttributeValue refElement, String xdefRefValue - ) { + private PsiReference[] getReferencesFromXDefRef(XmlAttributeValue attrValueElement, String attrValue) { // - /nop/schema/xdef.xdef: // - `` // - `` @@ -223,35 +237,75 @@ public class XLangReferenceProvider extends PsiReferenceProvider { // `` // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, xdefRefValue.length() + 1); + TextRange textRange = new TextRange(1, attrValue.length() + 1); + String ref; + String path = null; + List psiFiles; // 含有后缀的,视为文件引用 - if (xdefRefValue.indexOf(".") > 0) { - int hashIndex = xdefRefValue.indexOf('#'); + if (attrValue.indexOf(".") > 0) { + int hashIndex = attrValue.indexOf('#'); + + path = hashIndex > 0 ? attrValue.substring(0, hashIndex) : attrValue; + ref = hashIndex > 0 ? attrValue.substring(hashIndex + 1) : null; - String path = hashIndex > 0 ? xdefRefValue.substring(0, hashIndex) : xdefRefValue; - String ref = hashIndex > 0 ? xdefRefValue.substring(hashIndex + 1) : null; - NopVfsFileReference.Anchor anchor = new NopVfsFileReference.RefAnchor(ref); + // 文件引用直接返回 + if (ref == null) { + return getReferencesByVfsPath(attrValueElement, path, textRange, null); + } - return getReferencesByVfsPath(project, refElement, path, textRange, anchor); + psiFiles = XmlPsiHelper.findPsiFilesByNopVfsPath(attrValueElement, path); } // 否则,视为名字引用 else { - String ref = xdefRefValue; - NopVfsFileReference.Anchor anchor = new NopVfsFileReference.RefAnchor(ref); + ref = attrValue; // Note: 只能引用当前文件(不一定是 vfs)内的名字 - PsiFile file = refElement.getContainingFile(); + psiFiles = Collections.singletonList(attrValueElement.getContainingFile()); + } - return new PsiReference[] { - new NopVfsFileReference(refElement, textRange, file, anchor) - }; + // 收集引用节点属性 + List targets = new ArrayList<>(); + psiFiles.forEach((file) -> { + XmlElement target = XmlPsiHelper.findFirstElement(file, (element) -> { + if (element instanceof XmlAttribute attr) { + String name = attr.getName(); + String value = attr.getValue(); + + // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 + return ("xdef:name".equals(name) // + || "meta:name".equals(name) // + ) && ref.equals(value); + } + return false; + }); + + if (target != null) { + targets.add(target); + } + }); + + PsiReference[] refs = targets.stream() + .map((attr) -> new XLangElementReference(attrValueElement, textRange, attr)) + .toArray(PsiReference[]::new); + if (refs.length > 0) { + return refs; } + + String msg = path == null + ? NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found", ref) + : NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found-in-path", ref, path); + return new PsiReference[] { + new XLangNotFoundReference(attrValueElement, textRange, msg) + }; } /** 从 x:prototype 的属性值中获取引用 */ private PsiReference[] getReferencesFromPrototype( - Project project, XmlAttributeValue refElement, XmlTagInfo tagInfo, String attrValue + XmlAttributeValue attrValueElement, String attrValue, XmlTagInfo tagInfo ) { + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange textRange = new TextRange(1, attrValue.length() + 1); + // 仅从父节点中取引用到的子节点 // io.nop.xlang.delta.DeltaMerger#mergePrototype IXDefNode defNode = tagInfo.getDefNode(); @@ -264,68 +318,93 @@ public class XLangReferenceProvider extends PsiReferenceProvider { XmlTag parentTag = tagInfo.getTag().getParentTag(); if (parentTag == null) { - return PsiReference.EMPTY_ARRAY; + String msg = NopPluginBundle.message("xlang.annotation.reference.x-prototype-no-parent"); + return new PsiReference[] { + new XLangNotFoundReference(attrValueElement, textRange, msg) + }; } XmlTag protoTag = XmlPsiHelper.getChildTagByAttr(parentTag, keyAttr, attrValue); if (protoTag == null) { - return PsiReference.EMPTY_ARRAY; + String msg = keyAttr == null + ? NopPluginBundle.message("xlang.annotation.reference.x-prototype-tag-not-found", + attrValue) + : NopPluginBundle.message("xlang.annotation.reference.x-prototype-attr-not-found", + keyAttr, + attrValue); + return new PsiReference[] { + new XLangNotFoundReference(attrValueElement, textRange, msg) + }; } - // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, attrValue.length() + 1); + // 定位到目标属性或标签上 + XmlElement target = keyAttr != null ? protoTag.getAttribute(keyAttr) : protoTag; - return new PsiReference[] { new XLangElementReference(refElement, textRange, protoTag) }; + return new PsiReference[] { new XLangElementReference(attrValueElement, textRange, target) }; } /** 从 xdef:key-attr 的属性值中获取引用 */ private PsiReference[] getReferencesFromKeyAttr( - Project project, XmlAttributeValue refElement, XmlTagInfo tagInfo, String attrValue + XmlAttributeValue attrValueElement, String attrValue, XmlTagInfo tagInfo ) { // Note: XmlAttributeValue 的文本范围是包含引号的 TextRange textRange = new TextRange(1, attrValue.length() + 1); - return XmlPsiHelper.getAttrsFromChildTag(tagInfo.getTag(), attrValue) - .stream() - .map((attr) -> new XLangElementReference(refElement, textRange, attr)) - .toArray(PsiReference[]::new); + PsiReference[] refs = XmlPsiHelper.getAttrsFromChildTag(tagInfo.getTag(), attrValue) + .stream() + .map((attr) -> new XLangElementReference(attrValueElement, textRange, attr)) + .toArray(PsiReference[]::new); + if (refs.length > 0) { + return refs; + } + + String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-key-attr-not-found", attrValue); + return new PsiReference[] { + new XLangNotFoundReference(attrValueElement, textRange, msg) + }; } /** 从 xdef:unique-attr 的属性值中获取引用 */ private PsiReference[] getReferencesFromUniqueAttr( - Project project, XmlAttributeValue refElement, XmlTagInfo tagInfo, String attrValue + XmlAttributeValue attrValueElement, String attrValue, XmlTagInfo tagInfo ) { + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange textRange = new TextRange(1, attrValue.length() + 1); + // 仅从当前节点中取引用到的属性 XmlTag tag = tagInfo.getTag(); XmlAttribute attr = tag.getAttribute(attrValue); if (attr == null) { - return PsiReference.EMPTY_ARRAY; + String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-unique-attr-no-found", attrValue); + return new PsiReference[] { + new XLangNotFoundReference(attrValueElement, textRange, msg) + }; } - // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, attrValue.length() + 1); - - return new PsiReference[] { new XLangElementReference(refElement, textRange, attr) }; + return new PsiReference[] { new XLangElementReference(attrValueElement, textRange, attr) }; } private PsiReference[] getReferencesByVfsPath( - Project project, XmlElement refElement, // - String path, TextRange textRange, NopVfsFileReference.Anchor anchor + XmlElement refElement, String path, // + TextRange textRange, XLangVfsFileReference.Anchor anchor ) { if (!StringHelper.isValidFilePath(path) || path.indexOf('.') <= 0) { return PsiReference.EMPTY_ARRAY; } - String absPath = XmlPsiHelper.absolutePath(path, refElement); - - PsiReference[] refs = XmlPsiHelper.findPsiFileList(project, absPath) + PsiReference[] refs = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, path) .stream() - .map((file) -> new NopVfsFileReference(refElement, textRange, file, anchor)) + .map((file) -> new XLangVfsFileReference(refElement, textRange, file, anchor)) .toArray(PsiReference[]::new); - return refs.length > 0 // - ? refs // - : new PsiReference[] { new NopVfsFileReference.NotFound(refElement, textRange, path) }; + if (refs.length > 0) { + return refs; + } + + String msg = NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", path); + return new PsiReference[] { + new XLangNotFoundReference(refElement, textRange, msg) + }; } private Map extractPathsFromCsv(String csv) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java similarity index 47% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java index 68461e466..8a247fe04 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/NopVfsFileReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java @@ -1,14 +1,9 @@ package io.nop.idea.plugin.reference; -import java.util.ArrayList; -import java.util.List; - import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReferenceBase; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; @@ -23,7 +18,7 @@ import org.jetbrains.annotations.Nullable; * @author flytreeleft * @date 2025-06-22 */ -public class NopVfsFileReference extends PsiReferenceBase implements XLangReference { +public class XLangVfsFileReference extends PsiReferenceBase implements XLangReference { private final PsiFile file; /** 文件内的定位锚点 */ private final Anchor anchor; @@ -36,7 +31,7 @@ public class NopVfsFileReference extends PsiReferenceBase implements * 指定对应引用的关联文本所在的文本范围,该范围相对于该元素,从 0 开始计算。 * 相关处理逻辑见 {@link com.intellij.psi.impl.SharedPsiElementImplUtil#addReferences SharedPsiElementImplUtil#addReferences}) */ - public NopVfsFileReference( + public XLangVfsFileReference( @NotNull XmlElement refElement, TextRange textRange, // @NotNull PsiFile file, Anchor anchor ) { @@ -50,34 +45,15 @@ public class NopVfsFileReference extends PsiReferenceBase implements /** 得到具体的引用对象(文件、文件行、文件内某个元素等) */ @Override public @Nullable PsiElement resolve() { - List results = new ArrayList<>(); + PsiElement result = null; if (anchor instanceof PosAnchor pos) { - PsiElement el = XmlPsiHelper.getPsiElementAt(file, pos.line, pos.column); - if (el != null) { - results.add(el); - } - } else { - PsiTreeUtil.processElements(file, element -> { - if (!(element instanceof XmlElement el)) { - return true; // 跳过非 xml 元素 - } - - if (anchor == null) { - if (el instanceof XmlTag) { - results.add(el); - } - } else if (anchor instanceof RefAnchor ref) { - if (ref.match(el)) { - results.add(el); - } - } - // 仅取第一个匹配到的元素,否则,继续遍历 - return results.isEmpty(); - }); + result = XmlPsiHelper.getPsiElementAt(file, pos.line, pos.column); + } else if (anchor == null) { + result = XmlPsiHelper.findFirstElement(file, (element) -> element instanceof XmlTag); } - return results.isEmpty() ? this.file : results.get(0); + return result == null ? this.file : result; } /** 得到补全建议元素列表,可以为字符串或 {@link PsiElement} */ @@ -91,56 +67,7 @@ public class NopVfsFileReference extends PsiReferenceBase implements return target instanceof PsiFile && ((PsiFile) target).getVirtualFile().getPath().endsWith("/_vfs" + file); } - public static class NotFound extends PsiReferenceBase implements XLangReference { - private final String path; - - public NotFound(@NotNull XmlElement refElement, TextRange textRange, String path) { - super(refElement, textRange, false); - this.path = path; - } - - public String getPath() { - return path; - } - - @Override - public @Nullable PsiElement resolve() { - return null; - } - - @Override - public @NotNull Object @NotNull [] getVariants() { - return ArrayUtil.EMPTY_OBJECT_ARRAY; - } - - @Override - public boolean isReferenceTo(@NotNull PsiElement target) { - return false; - } - } - public interface Anchor {} public record PosAnchor(int line, int column) implements Anchor {} - - public record RefAnchor(String value) implements Anchor { - - public boolean match(XmlElement element) { - if (value == null) { - // 若未引用名字,则匹配 xml 根节点 - return element instanceof XmlTag; - } // - else if (element instanceof XmlAttribute attr) { - String attrName = attr.getName(); - String attrValue = attr.getValue(); - - // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 - return ("xdef:name".equals(attrName) // - || "meta:name".equals(attrName) // - ) && value.equals(attrValue); - } - - return false; - } - } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 5d46e3358..e05fa1f7e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -15,6 +15,7 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; import com.intellij.lang.ASTNode; import com.intellij.openapi.editor.Document; @@ -27,9 +28,9 @@ import com.intellij.psi.PsiManager; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; -import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTokenType; @@ -37,18 +38,34 @@ import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.core.resource.ResourceHelper; import io.nop.xlang.xpl.xlib.XplLibHelper; +import org.jetbrains.annotations.NotNull; public class XmlPsiHelper { - public static String absolutePath(String path, XmlElement element) { + public static String getNopVfsPath(PsiElement element) { + PsiFile file = element.getContainingFile(); + if (file == null) { + return null; + } + + VirtualFile vf = file.getVirtualFile(); + if (vf == null) { + return null; + } + + return ProjectFileHelper.getNopVfsPath(vf); + } + + public static String getNopVfsAbsolutePath(String path, PsiElement element) { String filePath = getNopVfsPath(element); + return StringHelper.absolutePath(filePath, path); } public static List findPsiFileList(Project project, String path) { String fileName = StringHelper.fileFullName(path); Collection vfList = FilenameIndex.getVirtualFilesByName(fileName, - GlobalSearchScope.allScope(project)); + GlobalSearchScope.allScope(project)); if (vfList.isEmpty()) { return Collections.emptyList(); } @@ -67,7 +84,7 @@ public class XmlPsiHelper { return ret; } - public static PsiFile[] findPsiFile(Project project, String path) { + public static PsiFile[] findPsiFiles(Project project, String path) { List list = findPsiFileList(project, path); if (list.isEmpty()) { return PsiFile.EMPTY_ARRAY; @@ -75,6 +92,13 @@ public class XmlPsiHelper { return list.toArray(PsiFile.EMPTY_ARRAY); } + public static List findPsiFilesByNopVfsPath(PsiElement element, String path) { + Project project = element.getProject(); + String absPath = getNopVfsAbsolutePath(path, element); + + return XmlPsiHelper.findPsiFileList(project, absPath); + } + public static List findXplLib(Project project, XmlTag tag) { String ns = StringHelper.getNamespace(tag.getName()); if ("thisLib".equals(ns)) { @@ -158,20 +182,6 @@ public class XmlPsiHelper { return type == XmlElementType.XML_NAME || type == XmlElementType.XML_TAG_NAME || type == XmlElementType.XML_TAG; } - public static String getNopVfsPath(PsiElement element) { - PsiFile file = element.getContainingFile(); - if (file == null) { - return null; - } - - VirtualFile vf = file.getVirtualFile(); - if (vf == null) { - return null; - } - - return ProjectFileHelper.getNopVfsPath(vf); - } - /** 获取指定行列的 {@link PsiElement 元素} */ public static PsiElement getPsiElementAt(PsiFile psiFile, int line, int column) { Document document = psiFile.getViewProvider().getDocument(); @@ -278,7 +288,7 @@ public class XmlPsiHelper { } /** - * 根据属性值获取匹配的子节点,在 attrName$type 时,匹配节点的标签名 + * 根据属性值获取匹配的子节点,在 attrNamenull 时,匹配节点的标签名 *

* 其逻辑等价于 {@link io.nop.core.lang.xml.XNode#childByAttr} */ @@ -316,6 +326,22 @@ public class XmlPsiHelper { return attrs; } + /** 找到第一个符合条件的 {@link PsiElement 元素} */ + public static T findFirstElement( + PsiElement element, Predicate condition + ) { + PsiElement[] result = new PsiElement[] { null }; + + PsiTreeUtil.processElements(element, el -> { + if (condition.test(el)) { + result[0] = el; + return false; + } + return true; // 继续遍历 + }); + return (T) result[0]; + } + public static XmlTag getXmlTag(PsiElement element) { if (element == null) { return null; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java index 6c984498f..4dea8eca5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java @@ -220,6 +220,12 @@ public class XmlTagInfo { private DefAttrWithNode getDefAttrInfo(String attrName) { // 为 xmlns 节点构造属性 if (isXmlns(attrName)) { + String attrValue = tag.getAttributeValue(attrName); + // 忽略 xmlns:biz="biz" 形式的属性 + if (attrName.endsWith(":" + attrValue)) { + return null; + } + XDefTypeDecl type = new XDefTypeDeclParser().parseFromText(null, XDefConstants.STD_DOMAIN_XDEF_REF); XDefAttribute attr = new XDefAttribute(); diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties index bc0af2e95..d9e778282 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties @@ -6,6 +6,13 @@ xlang.annotation.attr.not-allow-empty=Attr ''{0}'' not allow empty xlang.annotation.value.not-allow-empty=Tag ''{0}'' value not allow empty xlang.annotation.tag.not-allow-value=Tag ''{0}'' not allow value xlang.annotation.tag.not-allow-child=Tag ''{0}'' not allow child -xlang.annotation.reference.vfs-file-not-found=The referenced vfs file ''{0}'' doesn''t exist yet +xlang.annotation.reference.vfs-file-not-found=The referenced vfs file ''{0}'' doesn''t exist +xlang.annotation.reference.xdef-ref-not-found=No xdef defined node named ''{0}'' exists +xlang.annotation.reference.xdef-ref-not-found-in-path=No xdef defined node named ''{0}'' in ''{1}'' +xlang.annotation.reference.xdef-unique-attr-no-found=No attribute named ''{0}'' in current node +xlang.annotation.reference.xdef-key-attr-not-found=No child node which has attribute named ''{0}'' exists +xlang.annotation.reference.x-prototype-no-parent=Only child node can define ''x:prototype'' +xlang.annotation.reference.x-prototype-tag-not-found=No sibling node named ''{0}'' exists +xlang.annotation.reference.x-prototype-attr-not-found=No sibling node which has attribute ''{0}={1}'' exists xlang.doc.markdown.link-title='{'Link'}'{0} xlang.doc.markdown.image-title='{'Image'}'{0} diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties index 0457b255f..2acfa0d1a 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties @@ -7,5 +7,12 @@ xlang.annotation.value.not-allow-empty=\u6807\u7B7E ''{0}'' \u7684\u503C\u4E0D\u xlang.annotation.tag.not-allow-value=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5185\u5BB9\u8282\u70B9 xlang.annotation.tag.not-allow-child=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5B50\u8282\u70B9 xlang.annotation.reference.vfs-file-not-found=\u5F15\u7528\u7684 vfs \u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 +xlang.annotation.reference.xdef-ref-not-found=\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 +xlang.annotation.reference.xdef-ref-not-found-in-path=\u5728 ''{1}'' \u4E2D\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 +xlang.annotation.reference.xdef-unique-attr-no-found=\u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 +xlang.annotation.reference.xdef-key-attr-not-found=\u5728\u5B50\u8282\u70B9\u4E2D\u672A\u5B9A\u4E49\u5C5E\u6027 ''{0}'' +xlang.annotation.reference.x-prototype-no-parent=\u53EA\u6709\u5728\u5B50\u8282\u70B9\u4E0A\u624D\u80FD\u5B9A\u4E49\u5C5E\u6027 ''x:prototype'' +xlang.annotation.reference.x-prototype-tag-not-found=\u4E0D\u5B58\u5728\u6807\u7B7E\u540D\u4E3A ''{0}'' \u7684\u5144\u5F1F\u8282\u70B9 +xlang.annotation.reference.x-prototype-attr-not-found=\u4E0D\u5B58\u5728\u5C5E\u6027\u4E3A ''{0}={1}'' \u7684\u5144\u5F1F\u8282\u70B9 xlang.doc.markdown.link-title='{'\u94FE\u63A5'}'{0} xlang.doc.markdown.image-title='{'\u56FE\u7247'}'{0} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index e74b31ddc..ed432c14e 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -43,6 +43,35 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { } public void testGetReferencesFromXmlAttributeValue() { + // 对 v-path 属性值的引用 + // - x:schema=v-path + doTest(""" + + """, "/nop/schema/xmeta.xdef"); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", + "x:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", + "xdsl:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + // - xdef:default-extends=v-path + doTest(""" + + """, "/test/link/default.xform"); + // - xpl:lib=v-path + doTest(""" + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); // 对 v-path-list 列表元素的引用 doTest(""" fNode\""), "#XDefNode"); + "meta:ref=\"XDefNode\""), + "meta:define#meta:name=XDefNode"); doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), - "#DslNode"); + "xdef:unknown-tag#xdef:name=DslNode"); + doTest(""" + + + + + """, "xdef:define#xdef:name=PropNode"); + doTest(""" + + + + + """, null); // - 在 *.xdef 中引用外部文件 doTest(""" - """, "/test/link/test-filter.xdef#FilterCondition"); + """, "xdef:define#xdef:name=FilterCondition"); + // - 外部文件中的引用节点不存在 + doTest(""" + + """, null); // 对 x:prototype 属性值的引用 doTest(readVfsResource("/test/link/user.view.xml").replace("x:prototype=\"list\"", - "x:prototype=\"list\""), "grid#list"); + "x:prototype=\"list\""), "grid#id=list"); doTest(readVfsResource("/test/link/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), "Get"); + // - 引用不存在 + doTest(""" + + + + + + """, null); + doTest(""" + + + + + + """, null); // 对唯一键的引用 doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), "xdef:prop#name"); + "meta:unique-attr=\"name\""), + "xdef:prop#name=!xml-name"); doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"xdef:name\"", "meta:unique-attr=\"xdef:name\""), - "xdef:define#xdef:name"); + "xdef:define#xdef:name=!var-name"); doTest(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"id\"", "xdef:key-attr=\"id\""), - "selection#id"); - - // 缺省:对有效文件的引用 - doTest(""" - - """, "/test/link/a.xlib"); - doTest(""" - - """, "/test/link/a.xlib"); + "selection#id=!var-name"); + // - 引用不存在 doTest(""" -

- """, "/test/link/default.xform"); - // - x:schema=v-path - doTest(""" - - """, "/nop/schema/xmeta.xdef"); - // - xdef:default-extends=v-path - doTest(""" - - """, "/test/link/default.xform"); - // - xpl:lib=v-path + + + + """, null); doTest(""" - - - - - - """, "/nop/core/xlib/meta-gen.xlib"); + + + + + """, null); + + // TODO 对 xpl 属性的文件引用 +// doTest(""" +// +// """, "/test/link/a.xlib"); +// doTest(""" +// +// """, "/test/link/a.xlib"); doTest(""" """, "/nop/core/xlib/meta-gen.xlib"); - // - x:schema=v-path - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", - "x:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", - "xdsl:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); + + // 非有效路径或未定义属性引用 + doTest(readVfsResource("/test/link/user.view.xml").replace("xmlns:view-gen=\"view-gen\"", + "xmlns:view-gen=\"view-gen\""), null); + doTest(""" + + """, null); + + // 未知 schema 导致引用无法识别,但支持对 *.xdef 的引用识别 + doTest(""" + + """, null); + doTest(""" + + """, "/nop/schema/xdsl.xdef"); // // TODO 声明属性将引用属性的类型定义 // doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", @@ -173,33 +242,42 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // 其会按 PsiMultiReference#COMPARATOR 对引用排序得到优先引用, // 再调用该优先引用的 #resolve() 得到 PsiElement PsiReference ref = findReferenceAtCaret(); + if (!(ref instanceof XLangReference)) { + if (expected == null) { + return; // 不检查非 XLang 引用 + } + assertInstanceOf(ref, PsiMultiReference.class); ref = ((PsiMultiReference) ref).getReferences()[0]; - assertInstanceOf(ref, XLangReference.class); } + assertInstanceOf(ref, XLangReference.class); PsiElement target = ref.resolve(); + if (expected == null) { + assertNull(target); + return; + } assertNotNull(target); - if (ref instanceof NopVfsFileReference) { + if (ref instanceof XLangVfsFileReference) { // Note: 可能不是 vfs 文件 String vfsPath = XmlPsiHelper.getNopVfsPath(target); String anchor = target instanceof XmlAttribute attr ? attr.getValue() : null; assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); - } else if (ref instanceof XLangElementReference) { + } // + else if (ref instanceof XLangElementReference) { assertInstanceOf(target, XmlElement.class); if (target instanceof XmlTag tag) { - String id = tag.getAttributeValue("id"); - - assertEquals(expected, tag.getName() + (id != null ? "#" + id : "")); - } else if (target instanceof XmlAttribute attr) { + assertEquals(expected, tag.getName()); + } // + else if (target instanceof XmlAttribute attr) { XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); - assertEquals(expected, tag.getName() + "#" + attr.getName()); + assertEquals(expected, tag.getName() + "#" + attr.getName() + "=" + attr.getValue()); } else { fail("Unknown target " + target.getClass()); } -- Gitee From 5c7756d36e2b023decb3eda78a9383abfed6885a Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 24 Jun 2025 21:14:02 +0800 Subject: [PATCH 16/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=EF=BC=8C=E5=8F=AF=20ctrl=20+=20?= =?UTF-8?q?=E7=82=B9=E5=87=BB=20=E5=B1=9E=E6=80=A7=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E5=88=B0=E8=AF=A5=E5=B1=9E=E6=80=A7=E7=9A=84=20xdef=20?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 3 +- .../plugin/lang/XLangFileTypeDetector.java | 15 +- .../reference/XLangReferenceProvider.java | 135 +++++++++--------- .../reference/XLangVfsFileReference.java | 18 +-- .../plugin/reference/XLangXDefReference.java | 28 ++++ .../nop/idea/plugin/utils/XmlPsiHelper.java | 3 +- .../messages/NopPluginBundle.properties | 1 + .../messages/NopPluginBundle_zh.properties | 1 + .../idea/plugin/BaseXLangPluginTestCase.java | 33 ++++- .../doc/TestXLangDocumentationProvider.java | 8 +- .../link/TestXLangGotoDeclarationHandler.java | 8 +- .../reference/TestXLangReferenceProvider.java | 34 ++--- 12 files changed, 157 insertions(+), 130 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index b40e7ef0b..aa443060b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -50,7 +50,8 @@ public class XLangAnnotator implements Annotator { try { doAnnotate(element, holder); } catch (Exception e) { - holder.newAnnotation(HighlightSeverity.WARNING, e.getMessage()) + holder.newAnnotation(HighlightSeverity.WARNING, + e.getMessage() != null ? e.getMessage() : e.getClass().getName()) .highlightType(ProblemHighlightType.WARNING) .create(); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileTypeDetector.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileTypeDetector.java index 161eee68d..5cbbf292c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileTypeDetector.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileTypeDetector.java @@ -16,18 +16,23 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class XLangFileTypeDetector implements FileTypeRegistry.FileTypeDetector { + @Override - public @Nullable FileType detect(@NotNull VirtualFile file, @NotNull ByteSequence firstBytes, - @Nullable CharSequence firstCharsIfText) { - if (firstCharsIfText == null) + public @Nullable FileType detect( + @NotNull VirtualFile file, @NotNull ByteSequence firstBytes, @Nullable CharSequence firstCharsIfText + ) { + if (firstCharsIfText == null) { return null; + } String ext = file.getExtension(); - if (ext == null) + if (ext == null) { return null; + } - if (ext.equals("xpl") || ext.equals("xgen") || ext.equals("xrun")) + if (ext.equals("xdef") || ext.equals("xpl") || ext.equals("xgen") || ext.equals("xrun")) { return XLangFileType.INSTANCE; + } String schema = XLangFileHelper.getSchemaFromContent(firstCharsIfText); if (!StringHelper.isEmpty(schema)) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java index 6b301f0d1..a4c050ee8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; @@ -17,8 +18,8 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlElement; -import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; +import com.intellij.psi.xml.XmlText; import com.intellij.util.ProcessingContext; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; @@ -33,8 +34,6 @@ import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import org.jetbrains.annotations.NotNull; -import static io.nop.idea.plugin.utils.XmlPsiHelper.isElementType; - /** * 针对 XLang 中的 {@link PsiElement 元素} 创建引用 * @@ -67,32 +66,21 @@ public class XLangReferenceProvider extends PsiReferenceProvider { XmlToken:XML_ATTRIBUTE_VALUE_TOKEN('/nop/schema/xdef.xdef')(536,557) XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"')(557,558) */ - if (isElementType(element, XmlElementType.XML_NAME)) { - XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); - - if (attr != null) { - return getReferencesFromXmlAttribute((XmlElement) element, attr); - } else { - XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); - - if (tag != null) { - return getReferencesFromXmlTag((XmlElement) element, tag); - } - } + if (element instanceof XmlTag tag) { + return getReferencesFromXmlTag(tag); + } // + else if (element instanceof XmlAttribute attr) { + return getReferencesFromXmlAttribute(attr); } // - else if (isElementType(element, XmlElementType.XML_ATTRIBUTE_VALUE)) { - XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); + else if (element instanceof XmlAttributeValue value) { + XmlAttribute attr = PsiTreeUtil.getParentOfType(value, XmlAttribute.class); if (attr != null) { - return getReferencesFromXmlAttributeValue((XmlAttributeValue) element, attr); + return getReferencesFromXmlAttributeValue(value, attr); } } // - else if (isElementType(element, XmlElementType.XML_TEXT)) { - XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); - - if (tag != null) { - return getReferencesFromXmlText((XmlElement) element, tag); - } + else if (element.getParent() instanceof XmlText text) { + return getReferencesFromXmlText(text, text.getParentTag()); } return PsiReference.EMPTY_ARRAY; @@ -100,31 +88,50 @@ public class XLangReferenceProvider extends PsiReferenceProvider { } /** 获取 xml 标签对应的引用(节点定义、xpl 函数定义等) */ - private PsiReference @NotNull [] getReferencesFromXmlTag(XmlElement refElement, XmlTag tag) { + private PsiReference @NotNull [] getReferencesFromXmlTag(XmlTag tag) { // TODO xpl 函数的引用 return PsiReference.EMPTY_ARRAY; } /** 获取 xml 属性名对应的引用(属性定义) */ - private PsiReference @NotNull [] getReferencesFromXmlAttribute(XmlElement refElement, XmlAttribute attr) { + private PsiReference @NotNull [] getReferencesFromXmlAttribute(XmlAttribute attr) { String attrName = attr.getName(); XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); IXDefAttribute attrDef = tagInfo != null ? tagInfo.getDefAttr(attrName) : null; - if (attrDef == null) { + SourceLocation loc = attrDef != null ? attrDef.getLocation() : null; + if (loc == null) { return PsiReference.EMPTY_ARRAY; } - SourceLocation loc = attrDef.getLocation(); - String path = loc.getPath(); + // Note: 在 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 + String path = loc.getPath().replace("classpath:_vfs", ""); + // Note: 对于包含名字空间的属性,需仅对属性名建立引用,否则,会被默认的 xml 引用替代。 + // 不过,对于 XLang 而言,名字空间也无需建立引用 + TextRange textRange = new TextRange(attrName.indexOf(':') + 1, attrName.length()); - TextRange textRange = new TextRange(0, attrName.length()); + PsiReference[] refs = XmlPsiHelper.findPsiFilesByNopVfsPath(attr, path) + .stream() + .map((file) -> { + PsiElement element = XmlPsiHelper.getPsiElementAt(file, + loc.getLine(), + loc.getCol()); + return element instanceof XmlAttribute + ? (XmlAttribute) element + : PsiTreeUtil.getParentOfType(element, XmlAttribute.class); + }) + .filter(Objects::nonNull) + .map((defAttr) -> new XLangXDefReference(attr, textRange, defAttr)) + .toArray(PsiReference[]::new); + if (refs.length > 0) { + return refs; + } - return getReferencesByVfsPath(refElement, - path, - textRange, - new XLangVfsFileReference.PosAnchor(loc.getLine(), loc.getPos())); + String msg = NopPluginBundle.message("xlang.annotation.reference.attr-xdef-not-defined", attrName); + return new PsiReference[] { + new XLangNotFoundReference(attr, textRange, msg) + }; } /** 获取 xml 属性值对应的引用(文件、节点、属性类型等) */ @@ -189,7 +196,11 @@ public class XLangReferenceProvider extends PsiReferenceProvider { return getReferencesByDefault(refElement, attrValue); } - private PsiReference[] getReferencesFromXmlText(XmlElement refElement, XmlTag tag) { + private PsiReference[] getReferencesFromXmlText(XmlText refElement, XmlTag tag) { + if (tag == null) { + return PsiReference.EMPTY_ARRAY; + } + return PsiReference.EMPTY_ARRAY; } @@ -202,7 +213,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { // Note: XmlAttributeValue 的文本范围是包含引号的 TextRange textRange = new TextRange(1, attrValue.length() + 1); - return getReferencesByVfsPath(attrValueElement, attrValue, textRange, null); + return getReferencesByVfsPath(attrValueElement, attrValue, textRange); } /** 获取指定路径的引用(文件) */ @@ -210,7 +221,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { // Note: XmlAttributeValue 的文本范围是包含引号的 TextRange textRange = new TextRange(1, attrValue.length() + 1); - return getReferencesByVfsPath(attrValueElement, attrValue, textRange, null); + return getReferencesByVfsPath(attrValueElement, attrValue, textRange); } /** 从 csv 文本中获取引用 */ @@ -220,7 +231,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { List list = new ArrayList<>(rangePathMap.size()); rangePathMap.forEach((textRange, path) -> { - PsiReference[] refs = getReferencesByVfsPath(attrValueElement, path, textRange.shiftRight(1), null); + PsiReference[] refs = getReferencesByVfsPath(attrValueElement, path, textRange.shiftRight(1)); list.addAll(Arrays.stream(refs).toList()); }); @@ -251,7 +262,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { // 文件引用直接返回 if (ref == null) { - return getReferencesByVfsPath(attrValueElement, path, textRange, null); + return getReferencesByVfsPath(attrValueElement, path, textRange); } psiFiles = XmlPsiHelper.findPsiFilesByNopVfsPath(attrValueElement, path); @@ -264,29 +275,22 @@ public class XLangReferenceProvider extends PsiReferenceProvider { } // 收集引用节点属性 - List targets = new ArrayList<>(); - psiFiles.forEach((file) -> { - XmlElement target = XmlPsiHelper.findFirstElement(file, (element) -> { - if (element instanceof XmlAttribute attr) { - String name = attr.getName(); - String value = attr.getValue(); - - // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 - return ("xdef:name".equals(name) // - || "meta:name".equals(name) // - ) && ref.equals(value); - } - return false; - }); - - if (target != null) { - targets.add(target); - } - }); - - PsiReference[] refs = targets.stream() - .map((attr) -> new XLangElementReference(attrValueElement, textRange, attr)) - .toArray(PsiReference[]::new); + PsiReference[] refs = psiFiles.stream() + .map((file) -> (XmlAttribute) XmlPsiHelper.findFirstElement(file, (element) -> { + if (element instanceof XmlAttribute attr) { + String name = attr.getName(); + String value = attr.getValue(); + + // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 + return ("xdef:name".equals(name) // + || "meta:name".equals(name) // + ) && ref.equals(value); + } + return false; + })) + .filter(Objects::nonNull) + .map((attr) -> new XLangElementReference(attrValueElement, textRange, attr)) + .toArray(PsiReference[]::new); if (refs.length > 0) { return refs; } @@ -384,17 +388,14 @@ public class XLangReferenceProvider extends PsiReferenceProvider { return new PsiReference[] { new XLangElementReference(attrValueElement, textRange, attr) }; } - private PsiReference[] getReferencesByVfsPath( - XmlElement refElement, String path, // - TextRange textRange, XLangVfsFileReference.Anchor anchor - ) { + private PsiReference[] getReferencesByVfsPath(XmlElement refElement, String path, TextRange textRange) { if (!StringHelper.isValidFilePath(path) || path.indexOf('.') <= 0) { return PsiReference.EMPTY_ARRAY; } PsiReference[] refs = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, path) .stream() - .map((file) -> new XLangVfsFileReference(refElement, textRange, file, anchor)) + .map((file) -> new XLangVfsFileReference(refElement, textRange, file)) .toArray(PsiReference[]::new); if (refs.length > 0) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java index 8a247fe04..10917710c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java @@ -20,8 +20,6 @@ import org.jetbrains.annotations.Nullable; */ public class XLangVfsFileReference extends PsiReferenceBase implements XLangReference { private final PsiFile file; - /** 文件内的定位锚点 */ - private final Anchor anchor; /** * @param refElement @@ -33,25 +31,18 @@ public class XLangVfsFileReference extends PsiReferenceBase implemen */ public XLangVfsFileReference( @NotNull XmlElement refElement, TextRange textRange, // - @NotNull PsiFile file, Anchor anchor + @NotNull PsiFile file ) { super(refElement, textRange, // 不采用延迟解析模式,以确保当解析到有其他相关引用时,其能够被 PsiMultiReference 作为最优引用 false); this.file = file; - this.anchor = anchor; } /** 得到具体的引用对象(文件、文件行、文件内某个元素等) */ @Override public @Nullable PsiElement resolve() { - PsiElement result = null; - - if (anchor instanceof PosAnchor pos) { - result = XmlPsiHelper.getPsiElementAt(file, pos.line, pos.column); - } else if (anchor == null) { - result = XmlPsiHelper.findFirstElement(file, (element) -> element instanceof XmlTag); - } + PsiElement result = XmlPsiHelper.findFirstElement(file, (element) -> element instanceof XmlTag); return result == null ? this.file : result; } @@ -64,10 +55,7 @@ public class XLangVfsFileReference extends PsiReferenceBase implemen @Override public boolean isReferenceTo(@NotNull PsiElement target) { + // XmlAttributeReference#isReferenceTo return target instanceof PsiFile && ((PsiFile) target).getVirtualFile().getPath().endsWith("/_vfs" + file); } - - public interface Anchor {} - - public record PosAnchor(int line, int column) implements Anchor {} } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java new file mode 100644 index 000000000..d991d88ca --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java @@ -0,0 +1,28 @@ +package io.nop.idea.plugin.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.xml.XmlElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对 XLang 节点或属性的 XDef 定义元素的引用 + * + * @author flytreeleft + * @date 2025-06-24 + */ +public class XLangXDefReference extends PsiReferenceBase implements XLangReference { + private final XmlElement target; + + public XLangXDefReference(@NotNull XmlElement element, TextRange rangeInElement, XmlElement target) { + super(element, rangeInElement); + this.target = target; + } + + @Override + public @Nullable PsiElement resolve() { + return target; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index e05fa1f7e..35d2d713a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -22,6 +22,7 @@ import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; @@ -184,7 +185,7 @@ public class XmlPsiHelper { /** 获取指定行列的 {@link PsiElement 元素} */ public static PsiElement getPsiElementAt(PsiFile psiFile, int line, int column) { - Document document = psiFile.getViewProvider().getDocument(); + Document document = PsiDocumentManager.getInstance(psiFile.getProject()).getDocument(psiFile); assert document != null; int offset = document.getLineStartOffset(line - 1) + column - 1; diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties index d9e778282..03a323491 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties @@ -14,5 +14,6 @@ xlang.annotation.reference.xdef-key-attr-not-found=No child node which has attri xlang.annotation.reference.x-prototype-no-parent=Only child node can define ''x:prototype'' xlang.annotation.reference.x-prototype-tag-not-found=No sibling node named ''{0}'' exists xlang.annotation.reference.x-prototype-attr-not-found=No sibling node which has attribute ''{0}={1}'' exists +xlang.annotation.reference.attr-xdef-not-defined=Undefined attribute ''{0}'' xlang.doc.markdown.link-title='{'Link'}'{0} xlang.doc.markdown.image-title='{'Image'}'{0} diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties index 2acfa0d1a..ac1b7a223 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties @@ -14,5 +14,6 @@ xlang.annotation.reference.xdef-key-attr-not-found=\u5728\u5B50\u8282\u70B9\u4E2 xlang.annotation.reference.x-prototype-no-parent=\u53EA\u6709\u5728\u5B50\u8282\u70B9\u4E0A\u624D\u80FD\u5B9A\u4E49\u5C5E\u6027 ''x:prototype'' xlang.annotation.reference.x-prototype-tag-not-found=\u4E0D\u5B58\u5728\u6807\u7B7E\u540D\u4E3A ''{0}'' \u7684\u5144\u5F1F\u8282\u70B9 xlang.annotation.reference.x-prototype-attr-not-found=\u4E0D\u5B58\u5728\u5C5E\u6027\u4E3A ''{0}={1}'' \u7684\u5144\u5F1F\u8282\u70B9 +xlang.annotation.reference.attr-xdef-not-defined=\u5C5E\u6027 ''{0}'' \u672A\u5B9A\u4E49 xlang.doc.markdown.link-title='{'\u94FE\u63A5'}'{0} xlang.doc.markdown.image-title='{'\u56FE\u7247'}'{0} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 7c85784f6..8ae369190 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -8,6 +8,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; import io.nop.api.core.ApiConfigs; import io.nop.api.core.config.AppConfig; @@ -20,6 +21,7 @@ import io.nop.core.resource.IResource; import io.nop.core.resource.ResourceHelper; import io.nop.core.resource.VirtualFileSystem; import io.nop.idea.plugin.lang.XLangFileType; +import io.nop.idea.plugin.reference.XLangReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.xlang.initialize.XLangCoreInitializer; @@ -28,6 +30,8 @@ import io.nop.xlang.initialize.XLangCoreInitializer; * @date 2025-06-17 */ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtureTestCase { + private static final String XLANG_EXT = "xtest"; + private final Cancellable cleanup = new Cancellable(); @Override @@ -36,10 +40,11 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur // Note: 消除异常 "Write access is allowed inside write-action only" ApplicationManager.getApplication().runWriteAction(() -> { - // 临时注册 XLang 文件类型 - for (String ext : getXLangFileExtensions()) { - FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, ext); - } + // Note: *.xdef 等需显式注册,否则,这类文件会被视为二进制文件, + // 在通过 PsiDocumentManager 获取 Document 时,将返回 null + FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, "xdef"); + + FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, XLANG_EXT); }); // 初始化 XLang 环境:由于测试资源均在 classpath 中,故而,需采用默认的 ICoreInitializer 进行初始化, @@ -69,8 +74,8 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur super.tearDown(); } - protected String[] getXLangFileExtensions() { - return new String[0]; + protected void configureByXLangText(String text) { + myFixture.configureByText("unit." + XLANG_EXT, text); } /** @@ -95,8 +100,22 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur return myFixture.getFile().findElementAt(myFixture.getCaretOffset()); } + /** 找到光标位置的 {@link XLangReference} 或者其他类型的唯一引用 */ protected PsiReference findReferenceAtCaret() { - return TargetElementUtil.findReference(myFixture.getEditor(), myFixture.getCaretOffset()); + // 实际有多个引用时,将构造返回 PsiMultiReference, + // 其会按 PsiMultiReference#COMPARATOR 对引用排序得到优先引用, + // 再调用该优先引用的 #resolve() 得到 PsiElement + PsiReference ref = TargetElementUtil.findReference(myFixture.getEditor(), myFixture.getCaretOffset()); + + if (ref instanceof PsiMultiReference mref) { + for (PsiReference r : mref.getReferences()) { + if (r instanceof XLangReference) { + return r; + } + } + return ((PsiMultiReference) ref).getReferences()[0]; + } + return ref; } protected String getDoc() { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index fd5dc506e..e27319863 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -12,12 +12,6 @@ import io.nop.xlang.xdef.XDefConstants; * @date 2025-06-17 */ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { - private static final String XLANG_EXT = "xdoc"; - - @Override - protected String[] getXLangFileExtensions() { - return new String[] { XLANG_EXT }; - } public void testGenerateDocForXmlName() { // 显示标签文档 @@ -135,7 +129,7 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { /** 通过在 text 中插入 <caret> 代表光标位置 */ private void doTest(String text, Consumer checker) { - myFixture.configureByText("example." + XLANG_EXT, text); + configureByXLangText(text); String genDoc = getDoc(); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java index 4226ae445..46679fd88 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java @@ -13,12 +13,6 @@ import io.nop.idea.plugin.BaseXLangPluginTestCase; * @date 2025-06-17 */ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { - private static final String XLANG_EXT = "xgo"; - - @Override - protected String[] getXLangFileExtensions() { - return new String[] { XLANG_EXT }; - } @Override protected void setUp() throws Exception { @@ -218,7 +212,7 @@ public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { /** 通过在 text 中插入 <caret> 代表光标位置 */ private void doTest(String text, String... expected) { - myFixture.configureByText("example." + XLANG_EXT, text); + configureByXLangText(text); PsiElement[] refs = getGotoTargets(); assertNotNull(refs); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index ed432c14e..95ebd0c1a 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -2,7 +2,6 @@ package io.nop.idea.plugin.reference; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; -import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlElement; @@ -15,12 +14,6 @@ import io.nop.idea.plugin.utils.XmlPsiHelper; * @date 2025-06-22 */ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { - private static final String XLANG_EXT = "xref"; - - @Override - protected String[] getXLangFileExtensions() { - return new String[] { XLANG_EXT }; - } @Override protected void setUp() throws Exception { @@ -234,23 +227,24 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // ""); } + public void testGetReferencesFromXmlAttribute() { +// // 名字空间不做引用 +// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", +// "meta:unique-attr=\"name\""), null); + + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", + "meta:unique-attr=\"name\""), + "meta:define#xdef:unique-attr=xml-name"); + } + /** 通过在 text 中插入 <caret> 代表光标位置 */ private void doTest(String text, String expected) { - myFixture.configureByText("example." + XLANG_EXT, text); + configureByXLangText(text); - // 实际有多个引用时,将构造返回 PsiMultiReference, - // 其会按 PsiMultiReference#COMPARATOR 对引用排序得到优先引用, - // 再调用该优先引用的 #resolve() 得到 PsiElement PsiReference ref = findReferenceAtCaret(); - if (!(ref instanceof XLangReference)) { - if (expected == null) { - return; // 不检查非 XLang 引用 - } - - assertInstanceOf(ref, PsiMultiReference.class); - - ref = ((PsiMultiReference) ref).getReferences()[0]; + if (!(ref instanceof XLangReference) && expected == null) { + return; // 不检查非 XLang 引用 } assertInstanceOf(ref, XLangReference.class); @@ -268,7 +262,7 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); } // - else if (ref instanceof XLangElementReference) { + else if (ref instanceof XLangElementReference || ref instanceof XLangXDefReference) { assertInstanceOf(target, XmlElement.class); if (target instanceof XmlTag tag) { -- Gitee From bd2a37e95f5764b51a489c51d9fd56ab6713326d Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 25 Jun 2025 15:40:37 +0800 Subject: [PATCH 17/82] =?UTF-8?q?nop-idea-pugin:=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=AF=B9=20xdef:unknown-attr=20=E5=B1=9E=E6=80=A7=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 2 +- .../reference/XLangReferenceProvider.java | 34 ++- .../nop/idea/plugin/utils/XDefPsiHelper.java | 4 +- .../nop/idea/plugin/utils/XmlPsiHelper.java | 19 ++ .../io/nop/idea/plugin/utils/XmlTagInfo.java | 41 ++- .../link/TestXLangGotoDeclarationHandler.java | 259 ------------------ .../reference/TestXLangReferenceProvider.java | 136 +++++++-- .../test/resources/_vfs/test/doc/example.xdef | 6 + .../test/resources/_vfs/test/doc/example.xdoc | 5 + .../_vfs/test/{link => reference}/a.xlib | 0 .../_vfs/test/{link => reference}/a.xmeta | 0 .../_vfs/test/{link => reference}/b.xmeta | 2 +- .../_vfs/test/{link => reference}/c.xmeta | 0 .../test/{link => reference}/default.xform | 0 .../test/{link => reference}/test-filter.xdef | 0 .../test/{link => reference}/user.view.xml | 0 16 files changed, 191 insertions(+), 317 deletions(-) delete mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc rename nop-idea-plugin/src/test/resources/_vfs/test/{link => reference}/a.xlib (100%) rename nop-idea-plugin/src/test/resources/_vfs/test/{link => reference}/a.xmeta (100%) rename nop-idea-plugin/src/test/resources/_vfs/test/{link => reference}/b.xmeta (57%) rename nop-idea-plugin/src/test/resources/_vfs/test/{link => reference}/c.xmeta (100%) rename nop-idea-plugin/src/test/resources/_vfs/test/{link => reference}/default.xform (100%) rename nop-idea-plugin/src/test/resources/_vfs/test/{link => reference}/test-filter.xdef (100%) rename nop-idea-plugin/src/test/resources/_vfs/test/{link => reference}/user.view.xml (100%) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index aa443060b..a03f5f7c9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -66,7 +66,7 @@ public class XLangAnnotator implements Annotator { ) { holder.newSilentAnnotation(HighlightSeverity.INFORMATION) .range(reference.getAbsoluteRange()) - .textAttributes(DefaultLanguageHighlighterColors.CLASS_REFERENCE) + .textAttributes(DefaultLanguageHighlighterColors.DOC_COMMENT_TAG_VALUE) .create(); } else if (reference instanceof XLangNotFoundReference ref) { holder.newAnnotation(HighlightSeverity.ERROR, ref.getMessage()) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java index a4c050ee8..bcaa2e98e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -96,17 +96,22 @@ public class XLangReferenceProvider extends PsiReferenceProvider { /** 获取 xml 属性名对应的引用(属性定义) */ private PsiReference @NotNull [] getReferencesFromXmlAttribute(XmlAttribute attr) { - String attrName = attr.getName(); XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); - IXDefAttribute attrDef = tagInfo != null ? tagInfo.getDefAttr(attrName) : null; - - SourceLocation loc = attrDef != null ? attrDef.getLocation() : null; - if (loc == null) { + if (tagInfo == null) { return PsiReference.EMPTY_ARRAY; } - // Note: 在 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 - String path = loc.getPath().replace("classpath:_vfs", ""); + String attrName = attr.getName(); + IXDefAttribute attrDef = tagInfo.getDefAttr(attrName); + SourceLocation loc = attrDef != null ? attrDef.getLocation() : null; + + // Note: + // - 属性可能定义在外部 xdef 中 + // - SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 + String path = loc != null // + ? loc.getPath().replace("classpath:_vfs", "") // + : tagInfo.getDef().resourcePath(); + // Note: 对于包含名字空间的属性,需仅对属性名建立引用,否则,会被默认的 xml 引用替代。 // 不过,对于 XLang 而言,名字空间也无需建立引用 TextRange textRange = new TextRange(attrName.indexOf(':') + 1, attrName.length()); @@ -114,12 +119,15 @@ public class XLangReferenceProvider extends PsiReferenceProvider { PsiReference[] refs = XmlPsiHelper.findPsiFilesByNopVfsPath(attr, path) .stream() .map((file) -> { - PsiElement element = XmlPsiHelper.getPsiElementAt(file, - loc.getLine(), - loc.getCol()); - return element instanceof XmlAttribute - ? (XmlAttribute) element - : PsiTreeUtil.getParentOfType(element, XmlAttribute.class); + if (loc == null) { + // 尝试取 xdef:unknown-attr + SourceLocation sl = tagInfo.getDefNode().getLocation(); + XmlTag tag = XmlPsiHelper.getPsiElementAt(file, sl, XmlTag.class); + + return tag != null ? tag.getAttribute("xdef:unknown-attr") : null; + } else { + return XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class); + } }) .filter(Objects::nonNull) .map((defAttr) -> new XLangXDefReference(attr, textRange, defAttr)) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 0381abc0c..2b8fcaaf5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -91,7 +91,9 @@ public class XDefPsiHelper { String ns = getXDslNamespace(rootTag); String key = ns + ":schema"; - return rootTag.getAttributeValue(key); + String schemaUrl = rootTag.getAttributeValue(key); + // Note: schema 可能为相对路径 + return XmlPsiHelper.getNopVfsAbsolutePath(schemaUrl, rootTag); } public static IXDefinition loadSchema(String schemaUrl) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 35d2d713a..5e1303959 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -57,6 +57,10 @@ public class XmlPsiHelper { return ProjectFileHelper.getNopVfsPath(vf); } + /** + * 获取 path 的 vfs 绝对路径。 + * 若 path 为相对路径,则视为其相对于 element 所在文件的目录 + */ public static String getNopVfsAbsolutePath(String path, PsiElement element) { String filePath = getNopVfsPath(element); @@ -192,6 +196,21 @@ public class XmlPsiHelper { return psiFile.findElementAt(offset); } + /** 获取指定位置的 {@link PsiElement 元素} */ + public static PsiElement getPsiElementAt(PsiFile psiFile, SourceLocation loc) { + return getPsiElementAt(psiFile, loc.getLine(), loc.getCol()); + } + + /** 获取指定行列的指定类型的 {@link PsiElement 元素} */ + public static T getPsiElementAt(PsiFile psiFile, SourceLocation loc, Class type) { + PsiElement element = getPsiElementAt(psiFile, loc); + + if (type.isInstance(element)) { + return (T) element; + } + return PsiTreeUtil.getParentOfType(element, type); + } + public static SourceLocation getLocation(PsiElement element) { return getLocation(element, element.getTextOffset(), element.getTextLength()); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java index 4dea8eca5..e8fbcc310 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java @@ -163,10 +163,7 @@ public class XmlTagInfo { public XDefTypeDecl getDefAttrType(String attrName) { IXDefAttribute attr = getDefAttr(attrName); - if (attr == null) { - return defNode != null ? defNode.getXdefUnknownAttr() : null; - } - return attr.getType(); + return attr != null ? attr.getType() : null; } /** 获取节点注释 */ @@ -180,15 +177,11 @@ public class XmlTagInfo { return null; } - IXDefComment comment; + IXDefComment comment = null; DefAttrWithNode attr = getDefAttrInfo(attrName); - if (attr == null) { - comment = getDefNodeComment(); - // 若无属性实体,则取当前节点上的 xdef:unknown-attr 属性的注释 - attrName = "xdef:unknown-attr"; - } else { + if (attr != null) { comment = attr.node.getComment(); - attrName = attr.attr.getName(); + attrName = attr.attr.isUnknownAttr() ? "xdef:unknown-attr" : attr.attr.getName(); } return comment != null ? comment.getSubComments().get(attrName) : null; @@ -249,6 +242,13 @@ public class XmlTagInfo { return new DefAttrWithNode(xdslDefNode, attr); } + // 查找当前节点上声明的 xdef:unknown-attr 属性: + // 在当前 dsl 为 *.xdef(即,x:schema 为 /nop/schema/xdef.xdef)时有效 + attr = defNode != null ? defNode.getAttribute("xdef:unknown-attr") : null; + if (attr != null) { + return new DefAttrWithNode(defNode, attr); + } + // 针对 xdef.xdef 中的未确定属性:本质上都是 XDefNode 节点上的属性 if (isXDefNode()) { IXDefNode node = def.getXdefUnknownTag(); @@ -256,9 +256,24 @@ public class XmlTagInfo { return new DefAttrWithNode(node, node.getAttribute(attrName)); } - // Note: xdef:unknown-attr 只记录了类型,没有 IXDefAttribute 实体, + // Note: 在普通 *.xdef 的 IXDefNode 中, + // 对 xdef:unknown-attr 只记录了类型,并没有 IXDefAttribute 实体, // 其处理逻辑见 XDefinitionParser#parseNode + XDefTypeDecl xdefUnknownAttrType = defNode != null ? defNode.getXdefUnknownAttr() : null; + if (xdefUnknownAttrType != null) { + XDefAttribute at = new XDefAttribute() { + @Override + public boolean isUnknownAttr() { + return true; + } + }; + + at.setName(attrName); + at.setType(xdefUnknownAttrType); + + return new DefAttrWithNode(defNode, at); + } + return null; } - } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java deleted file mode 100644 index 46679fd88..000000000 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/link/TestXLangGotoDeclarationHandler.java +++ /dev/null @@ -1,259 +0,0 @@ -package io.nop.idea.plugin.link; - -import com.intellij.psi.PsiElement; -import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlFile; -import com.intellij.psi.xml.XmlTag; -import io.nop.idea.plugin.BaseXLangPluginTestCase; - -/** - * 参考 https://github.com/JetBrains/intellij-community/blob/master/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyGoToTypeDeclarationTest.java - * - * @author flytreeleft - * @date 2025-06-17 - */ -public class TestXLangGotoDeclarationHandler extends BaseXLangPluginTestCase { - - @Override - protected void setUp() throws Exception { - super.setUp(); - - // Note: 提前将需要跳转的文件添加到 Project 中 - addVfsResourcesToProject("/nop/schema/xdef.xdef", - "/nop/schema/xdsl.xdef", - "/nop/schema/xmeta.xdef", - "/nop/schema/xui/xview.xdef", - "/nop/schema/xui/store.xdef", - "/nop/core/xlib/meta-gen.xlib", - "/nop/schema/schema/obj-schema.xdef", - "/dict/test/doc/child-type.dict.yaml", - "/test/link/a.xmeta", - "/test/link/b.xmeta", - "/test/link/a.xlib", - "/test/link/default.xform", - "/test/link/test-filter.xdef"); - } - - public void testGetGotoDeclarationTargetsForXmlTag() { - } - - public void testGetGotoDeclarationTargetsForXmlAttributeValue() { - // 根据在 schema 中定义的属性类型决定跳转 - // - x:extends=v-path-list - doTest(""" - - """, "/test/link/a.xmeta"); - - // 对 xdef.xdef 中引用的跳转 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", - "x:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - - // 对 xdsl.xdef 中引用的跳转 - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", - "xdsl:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - - // 对 xdef-ref 类型属性的跳转 - // - 在 *.xdef 中引用内部名字 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", - "meta:ref=\"XDefNode\""), "XDefNode"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), - "DslNode"); - // - 在 *.xdef 中引用外部文件 - doTest(""" - - """, "/nop/schema/xdsl.xdef"); - doTest(""" - - """, "/nop/schema/schema/obj-schema.xdef"); - // - 在 *.xmeta 中引用外部文件中的节点 - doTest(""" - - """, "FilterCondition"); - - // 对 x:prototype 属性值的跳转 - doTest(readVfsResource("/test/link/user.view.xml").replace("x:prototype=\"list\"", - "x:prototype=\"list\""), "list"); - doTest(readVfsResource("/test/link/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), - "Get"); - - // 对唯一键的跳转 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), "name"); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"xdef:name\"", - "meta:unique-attr=\"xdef:name\""), "xdef:name"); - doTest(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"name\"", - "xdef:key-attr=\"name\""), "name"); - - // 缺省:任意有效的文件均可跳转 - doTest(""" - - """, "/test/link/a.xlib"); - doTest(""" - - """, "/test/link/a.xlib"); - doTest(""" - - """, "/test/link/default.xform"); - // - x:schema=v-path - doTest(""" - - """, "/nop/schema/xmeta.xdef"); - // - xdef:default-extends=v-path - doTest(""" - - """, "/test/link/default.xform"); - // - xpl:lib=v-path - doTest(""" - - - - - - """, "/nop/core/xlib/meta-gen.xlib"); - doTest(""" - - - - - - """, "/nop/core/xlib/meta-gen.xlib"); - doTest(""" - - - - - - - - - - """, "/nop/core/xlib/meta-gen.xlib"); - -// // TODO 声明属性仅跳转到属性的类型定义上 -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", -// "xdef:ref=\"xdef-ref\""), ""); -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("-name\""), ""); -// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", "x:schema=\"v-path\""), -// ""); - } - - public void testGetGotoDeclarationTargetsForXmlAttributePartialValue() { - // v-path-list 元素跳转 - doTest(""" - - """, "/test/link/b.xmeta"); - doTest(""" - - """, "/test/link/a.xmeta"); - - // TODO 字典/枚举的 options 跳转 - doTest(""" - - - - """, "/dict/test/doc/child-type.dict.yaml"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // - "io.nop.xlang.xdef.XDefOverride"); - - // TODO 字典/枚举的默认值跳转 - doTest(""" - - - - """, ""); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // - ""); - - // TODO 缺省属性值中 @attr: 引用跳转 - doTest(""" - - - - """, ""); - } - - public void testGetGotoDeclarationTargetsForXmlText() { - } - - /** 通过在 text 中插入 <caret> 代表光标位置 */ - private void doTest(String text, String... expected) { - configureByXLangText(text); - - PsiElement[] refs = getGotoTargets(); - assertNotNull(refs); - assertEquals(refs.length, expected.length); - - for (int i = 0; i < refs.length; i++) { - PsiElement ref = refs[i]; - String exp = expected[i]; - - // 引用文件 - if (ref instanceof XmlFile) { - String file = ((XmlFile) ref).getVirtualFile().toString(); - String actual = file.substring(file.indexOf("/_vfs/") + "/_vfs".length()); - - assertEquals(exp, actual); - } - // 引用节点 - else if (ref instanceof XmlTag tag) { - // Note: xdef.xdef 中的 meta:name 才是节点名 - String actual = tag.getAttributeValue("meta:name"); - if (actual == null) { - // 其他 *.xdef 中的节点名为 xdef:name - actual = tag.getAttributeValue("xdef:name"); - } - - if (actual == null) { - actual = tag.getAttributeValue("id"); - } - - // 缺省采用节点标签名 - if (actual == null) { - actual = tag.getName(); - } - - assertEquals(exp, actual); - } - // 引用属性 - else if (ref instanceof XmlAttribute attr) { - String actual = attr.getName(); - assertEquals(exp, actual); - } - } - } -} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index 95ebd0c1a..f08df6d3e 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -27,12 +27,14 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { "/nop/schema/xui/store.xdef", "/nop/core/xlib/meta-gen.xlib", "/nop/schema/schema/obj-schema.xdef", + "/nop/schema/schema/schema-node.xdef", "/dict/test/doc/child-type.dict.yaml", - "/test/link/a.xmeta", - "/test/link/b.xmeta", - "/test/link/a.xlib", - "/test/link/default.xform", - "/test/link/test-filter.xdef"); + "/test/doc/example.xdef", + "/test/reference/a.xmeta", + "/test/reference/b.xmeta", + "/test/reference/a.xlib", + "/test/reference/default.xform", + "/test/reference/test-filter.xdef"); } public void testGetReferencesFromXmlAttributeValue() { @@ -52,9 +54,9 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // - xdef:default-extends=v-path doTest(""" - """, "/test/link/default.xform"); + """, "/test/reference/default.xform"); // - xpl:lib=v-path doTest(""" - """, "/test/link/a.xmeta"); + """, "/test/reference/a.xmeta"); doTest(""" - """, "/test/link/b.xmeta"); + """, "/test/reference/b.xmeta"); // 对 xdef-ref 类型属性的引用 // - 在 *.xdef 中引用内部名字 @@ -116,21 +118,21 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { doTest(""" """, "xdef:define#xdef:name=FilterCondition"); // - 外部文件中的引用节点不存在 doTest(""" """, null); // 对 x:prototype 属性值的引用 - doTest(readVfsResource("/test/link/user.view.xml").replace("x:prototype=\"list\"", - "x:prototype=\"list\""), "grid#id=list"); - doTest(readVfsResource("/test/link/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), + doTest(readVfsResource("/test/reference/user.view.xml").replace("x:prototype=\"list\"", + "x:prototype=\"list\""), "grid#id=list"); + doTest(readVfsResource("/test/reference/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), "Get"); // - 引用不存在 doTest(""" @@ -175,13 +177,6 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { """, null); - // TODO 对 xpl 属性的文件引用 -// doTest(""" -// -// """, "/test/link/a.xlib"); -// doTest(""" -// -// """, "/test/link/a.xlib"); doTest(""" w-gen\""), null); + doTest(readVfsResource("/test/reference/user.view.xml").replace("xmlns:view-gen=\"view-gen\"", + "xmlns:view-gen=\"view-gen\""), null); doTest(""" - + """, null); // 未知 schema 导致引用无法识别,但支持对 *.xdef 的引用识别 @@ -218,6 +213,14 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { """, "/nop/schema/xdsl.xdef"); +// // TODO 对 xpl 属性的文件引用 +// doTest(""" +// +// """, "/test/reference/a.xlib"); +// doTest(""" +// +// """, "/test/reference/a.xlib"); +// // // TODO 声明属性将引用属性的类型定义 // doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", // "xdef:ref=\"xdef-ref\""), ""); @@ -227,14 +230,89 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // ""); } + public void testGetReferencesFromXmlAttributePartialValue() { + // TODO 字典/枚举的 options 引用 + doTest(""" + + + + """, "/dict/test/doc/child-type.dict.yaml"); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + "io.nop.xlang.xdef.XDefOverride"); + + // TODO 字典/枚举的默认值引用 + doTest(""" + + + + """, ""); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + ""); + + // TODO 缺省属性值中 @attr: 引用 + doTest(""" + + + + """, ""); + } + public void testGetReferencesFromXmlAttribute() { -// // 名字空间不做引用 -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", -// "meta:unique-attr=\"name\""), null); + // 名字空间不做引用 + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", + "meta:unique-attr=\"name\""), null); doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", "meta:unique-attr=\"name\""), "meta:define#xdef:unique-attr=xml-name"); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), + "xdef:prop#name=!xml-name"); + doTest(readVfsResource("/nop/schema/xdef.xdef").replace("ame=\"!var-name\""), + "xdef:define#xdef:name=!var-name"); + + doTest(readVfsResource("/test/doc/example.xdef").replace("name=\"string\"", "name=\"string\""), + "meta:define#xdef:unknown-attr=def-type"); + + doTest(""" + + pe="leaf"/> + + """, "child#type=dict:test/doc/child-type"); + doTest(""" + + ge="22"/> + + """, "child#xdef:unknown-attr=any"); + doTest(""" + + ge="23"/> + + """, "xdef:unknown-tag#xdef:unknown-attr=any"); + + doTest(""" + f="/test/reference/test-filter.xdef#FilterCondition" + /> + """, "schema#ref=xdef-ref"); } /** 通过在 text 中插入 <caret> 代表光标位置 */ diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index cb08b1a96..b5c91b987 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -15,4 +15,10 @@ @xdef:unknown-attr This a unknown attribute --> + + + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc new file mode 100644 index 000000000..ca3972ea7 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc @@ -0,0 +1,5 @@ + + + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib similarity index 100% rename from nop-idea-plugin/src/test/resources/_vfs/test/link/a.xlib rename to nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/a.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xmeta similarity index 100% rename from nop-idea-plugin/src/test/resources/_vfs/test/link/a.xmeta rename to nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xmeta diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/b.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/reference/b.xmeta similarity index 57% rename from nop-idea-plugin/src/test/resources/_vfs/test/link/b.xmeta rename to nop-idea-plugin/src/test/resources/_vfs/test/reference/b.xmeta index b4ee589a4..884f3b75d 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/link/b.xmeta +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/b.xmeta @@ -1,4 +1,4 @@ diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/c.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/reference/c.xmeta similarity index 100% rename from nop-idea-plugin/src/test/resources/_vfs/test/link/c.xmeta rename to nop-idea-plugin/src/test/resources/_vfs/test/reference/c.xmeta diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/default.xform b/nop-idea-plugin/src/test/resources/_vfs/test/reference/default.xform similarity index 100% rename from nop-idea-plugin/src/test/resources/_vfs/test/link/default.xform rename to nop-idea-plugin/src/test/resources/_vfs/test/reference/default.xform diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/test-filter.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/reference/test-filter.xdef similarity index 100% rename from nop-idea-plugin/src/test/resources/_vfs/test/link/test-filter.xdef rename to nop-idea-plugin/src/test/resources/_vfs/test/reference/test-filter.xdef diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/link/user.view.xml b/nop-idea-plugin/src/test/resources/_vfs/test/reference/user.view.xml similarity index 100% rename from nop-idea-plugin/src/test/resources/_vfs/test/link/user.view.xml rename to nop-idea-plugin/src/test/resources/_vfs/test/reference/user.view.xml -- Gitee From d7f9d72f6c5bc3cad3a7b6119bcb00446691f9db Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 25 Jun 2025 18:08:54 +0800 Subject: [PATCH 18/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E6=96=87=E6=9C=AC=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 9 + .../reference/XLangReferenceProvider.java | 177 ++++++++++-------- .../nop/idea/plugin/utils/XmlPsiHelper.java | 4 +- .../reference/TestXLangReferenceProvider.java | 43 ++++- .../test/resources/_vfs/test/doc/example.xdef | 2 + .../test/resources/_vfs/test/doc/example.xdoc | 3 + .../_vfs/test/reference/user.view.xml | 2 +- 7 files changed, 162 insertions(+), 78 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index a03f5f7c9..09454f935 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -18,6 +18,7 @@ import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlTag; +import com.intellij.psi.xml.XmlText; import com.intellij.psi.xml.XmlToken; import com.intellij.xml.util.XmlTagUtil; import io.nop.api.core.beans.DictBean; @@ -60,6 +61,14 @@ public class XLangAnnotator implements Annotator { } void doAnnotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + // Note: 在识别引用时,处理的是 XmlText 的 cdata 节点,因此,需要对其子节点做高亮处理 + if (element instanceof XmlText text) { + for (PsiElement child : text.getChildren()) { + doAnnotate(child, holder); + } + return; + } + for (PsiReference reference : element.getReferences()) { if (reference instanceof XLangVfsFileReference // || reference instanceof XLangElementReference // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java index bcaa2e98e..74abdf9ee 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -18,10 +18,12 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlElement; +import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; -import com.intellij.psi.xml.XmlText; import com.intellij.util.ProcessingContext; import io.nop.api.core.util.SourceLocation; +import io.nop.commons.text.MutableString; +import io.nop.commons.text.tokenizer.TextScanner; import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.messages.NopPluginBundle; import io.nop.idea.plugin.resource.ProjectEnv; @@ -34,6 +36,8 @@ import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import org.jetbrains.annotations.NotNull; +import static io.nop.idea.plugin.utils.XmlPsiHelper.isElementType; + /** * 针对 XLang 中的 {@link PsiElement 元素} 创建引用 * @@ -79,8 +83,12 @@ public class XLangReferenceProvider extends PsiReferenceProvider { return getReferencesFromXmlAttributeValue(value, attr); } } // - else if (element.getParent() instanceof XmlText text) { - return getReferencesFromXmlText(text, text.getParentTag()); + else if (isElementType(element, XmlElementType.XML_DATA_CHARACTERS)) { + XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); + + if (tag != null) { + return getReferencesFromXmlText((XmlElement) element, tag); + } } return PsiReference.EMPTY_ARRAY; @@ -166,34 +174,22 @@ public class XLangReferenceProvider extends PsiReferenceProvider { return PsiReference.EMPTY_ARRAY; } - // 根据属性声明的类型,对属性值做文件/名字引用跳转处理 - String stdDomain = attrDefType.getStdDomain(); - - // Note: v-path 类型采用缺省处理 - if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain) // - || XDefConstants.STD_DOMAIN_NAME_OR_V_PATH.equals(stdDomain) // - ) { - return getReferencesByVfsPath(refElement, attrValue); - } // - else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { - return getReferencesFromVfsPathCsv(refElement, attrValue); - } // - else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { - return getReferencesFromXDefRef(refElement, attrValue); - } // - else { - String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); + // 根据属性声明的类型,对属性值做文件/名字引用 + PsiReference[] refs = getReferencesByDefType(refElement, attrValue, attrDefType); + if (refs != null) { + return refs; + } - if ((xdslNs + ":prototype").equals(attrName)) { - return getReferencesFromPrototype(refElement, attrValue, tagInfo); - } else { - String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); + String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); + if ((xdslNs + ":prototype").equals(attrName)) { + return getReferencesFromPrototype(refElement, attrValue, tagInfo); + } else { + String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); - if ((xdefNs + ":key-attr").equals(attrName)) { - return getReferencesFromKeyAttr(refElement, attrValue, tagInfo); - } else if ((xdefNs + ":unique-attr").equals(attrName)) { - return getReferencesFromUniqueAttr(refElement, attrValue, tagInfo); - } + if ((xdefNs + ":key-attr").equals(attrName)) { + return getReferencesFromKeyAttr(refElement, attrValue, tagInfo); + } else if ((xdefNs + ":unique-attr").equals(attrName)) { + return getReferencesFromUniqueAttr(refElement, attrValue, tagInfo); } } @@ -204,42 +200,78 @@ public class XLangReferenceProvider extends PsiReferenceProvider { return getReferencesByDefault(refElement, attrValue); } - private PsiReference[] getReferencesFromXmlText(XmlText refElement, XmlTag tag) { - if (tag == null) { + private PsiReference[] getReferencesFromXmlText(XmlElement refElement, XmlTag tag) { + XmlTagInfo tagInfo = tag != null ? XDefPsiHelper.getTagInfo(tag) : null; + if (tagInfo == null) { return PsiReference.EMPTY_ARRAY; } - return PsiReference.EMPTY_ARRAY; - } - - /** 对文本做默认的引用识别 */ - private PsiReference[] getReferencesByDefault(XmlAttributeValue attrValueElement, String attrValue) { - if (!attrValue.endsWith(".xdef")) { + XDefTypeDecl tagDefType = tagInfo.getDefNode().getXdefValue(); + if (tagDefType == null) { return PsiReference.EMPTY_ARRAY; } - // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, attrValue.length() + 1); + String refValue = refElement.getText(); + PsiReference[] refs = getReferencesByDefType(refElement, refValue, tagDefType); + if (refs != null) { + return refs; + } + + return getReferencesByDefault(refElement, refValue); + } + + /** + * 根据数据域类型识别引用 + * + * @return 若返回 null,则表示未支持对指定类型的处理 + */ + private PsiReference[] getReferencesByDefType( + XmlElement refElement, String refValue, XDefTypeDecl refDefType + ) { + // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, + // 从而精确匹配与引用相关的文本内容 + int textRangeOffset = refElement.getText().indexOf(refValue); + TextRange textRange = new TextRange(textRangeOffset, refValue.length() + textRangeOffset); + + String stdDomain = refDefType.getStdDomain(); + + if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain) // + || XDefConstants.STD_DOMAIN_NAME_OR_V_PATH.equals(stdDomain) // + ) { + return getReferencesByVfsPath(refElement, refValue, textRange); + } // + else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { + return getReferencesFromVfsPathCsv(refElement, refValue, textRangeOffset); + } // + else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { + return getReferencesFromXDefRef(refElement, refValue, textRange); + } - return getReferencesByVfsPath(attrValueElement, attrValue, textRange); + return null; } - /** 获取指定路径的引用(文件) */ - private PsiReference[] getReferencesByVfsPath(XmlAttributeValue attrValueElement, String attrValue) { + /** 对文本做默认的引用识别 */ + private PsiReference[] getReferencesByDefault(XmlElement refElement, String refValue) { + if (!refValue.endsWith(".xdef")) { + return PsiReference.EMPTY_ARRAY; + } + // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, attrValue.length() + 1); + int textRangeOffset = refElement instanceof XmlAttributeValue ? 1 : 0; + TextRange textRange = new TextRange(textRangeOffset, refValue.length() + textRangeOffset); - return getReferencesByVfsPath(attrValueElement, attrValue, textRange); + return getReferencesByVfsPath(refElement, refValue, textRange); } /** 从 csv 文本中获取引用 */ - private PsiReference[] getReferencesFromVfsPathCsv(XmlAttributeValue attrValueElement, String attrValue) { - // Note: XmlAttributeValue 的文本范围是包含引号的 - Map rangePathMap = extractPathsFromCsv(attrValue); + private PsiReference[] getReferencesFromVfsPathCsv( + XmlElement refElement, String refValue, int textRangeOffset + ) { + Map rangePathMap = extractPathsFromCsv(refValue); List list = new ArrayList<>(rangePathMap.size()); rangePathMap.forEach((textRange, path) -> { - PsiReference[] refs = getReferencesByVfsPath(attrValueElement, path, textRange.shiftRight(1)); + PsiReference[] refs = getReferencesByVfsPath(refElement, path, textRange.shiftRight(textRangeOffset)); list.addAll(Arrays.stream(refs).toList()); }); @@ -248,38 +280,35 @@ public class XLangReferenceProvider extends PsiReferenceProvider { } /** 从 xdef-ref 类型的属性值中获取引用 */ - private PsiReference[] getReferencesFromXDefRef(XmlAttributeValue attrValueElement, String attrValue) { + private PsiReference[] getReferencesFromXDefRef(XmlElement refElement, String refValue, TextRange textRange) { // - /nop/schema/xdef.xdef: // - `` // - `` // - /nop/schema/schema/schema-node.xdef: // `` - // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, attrValue.length() + 1); - String ref; String path = null; List psiFiles; // 含有后缀的,视为文件引用 - if (attrValue.indexOf(".") > 0) { - int hashIndex = attrValue.indexOf('#'); + if (refValue.indexOf(".") > 0) { + int hashIndex = refValue.indexOf('#'); - path = hashIndex > 0 ? attrValue.substring(0, hashIndex) : attrValue; - ref = hashIndex > 0 ? attrValue.substring(hashIndex + 1) : null; + path = hashIndex > 0 ? refValue.substring(0, hashIndex) : refValue; + ref = hashIndex > 0 ? refValue.substring(hashIndex + 1) : null; // 文件引用直接返回 if (ref == null) { - return getReferencesByVfsPath(attrValueElement, path, textRange); + return getReferencesByVfsPath(refElement, path, textRange); } - psiFiles = XmlPsiHelper.findPsiFilesByNopVfsPath(attrValueElement, path); + psiFiles = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, path); } // 否则,视为名字引用 else { - ref = attrValue; + ref = refValue; // Note: 只能引用当前文件(不一定是 vfs)内的名字 - psiFiles = Collections.singletonList(attrValueElement.getContainingFile()); + psiFiles = Collections.singletonList(refElement.getContainingFile()); } // 收集引用节点属性 @@ -297,7 +326,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { return false; })) .filter(Objects::nonNull) - .map((attr) -> new XLangElementReference(attrValueElement, textRange, attr)) + .map((attr) -> new XLangElementReference(refElement, textRange, attr)) .toArray(PsiReference[]::new); if (refs.length > 0) { return refs; @@ -307,7 +336,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { ? NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found", ref) : NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found-in-path", ref, path); return new PsiReference[] { - new XLangNotFoundReference(attrValueElement, textRange, msg) + new XLangNotFoundReference(refElement, textRange, msg) }; } @@ -419,23 +448,21 @@ public class XLangReferenceProvider extends PsiReferenceProvider { private Map extractPathsFromCsv(String csv) { Map rangePathMap = new HashMap<>(); - int offset = 0; - for (int i = 0; i < csv.length(); i++) { - char ch = csv.charAt(i); - if (ch != ',') { - continue; - } + TextScanner sc = TextScanner.fromString(null, csv); - String path = csv.substring(offset, i); - rangePathMap.put(new TextRange(offset, i), path); + sc.skipBlank(); + while (!sc.isEnd()) { + int offset = sc.pos; + MutableString buf = sc.useBuf(); + sc.nextUntil(s -> s.cur == ',' || StringHelper.isSpace(sc.cur), sc::appendToBuf); - offset = i + 1; - } + String path = buf.toString(); + rangePathMap.put(new TextRange(offset, sc.pos), path); - if (offset < csv.length()) { - String path = csv.substring(offset); - rangePathMap.put(new TextRange(offset, csv.length()), path); + sc.next(); + sc.skipBlank(); } + return rangePathMap; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 5e1303959..f394f225a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -190,7 +190,9 @@ public class XmlPsiHelper { /** 获取指定行列的 {@link PsiElement 元素} */ public static PsiElement getPsiElementAt(PsiFile psiFile, int line, int column) { Document document = PsiDocumentManager.getInstance(psiFile.getProject()).getDocument(psiFile); - assert document != null; + if (document == null) { + return null; + } int offset = document.getLineStartOffset(line - 1) + column - 1; return psiFile.findElementAt(offset); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index f08df6d3e..2d3ba1b4c 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -221,7 +221,7 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // // """, "/test/reference/a.xlib"); // -// // TODO 声明属性将引用属性的类型定义 +// // TODO 声明属性将 引用 属性的类型定义 // doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", // "xdef:ref=\"xdef-ref\""), ""); // doTest(readVfsResource("/nop/schema/xdef.xdef").replace("", ""), + "/test/reference/a.xmeta"); + + doTest(""" + + /test/reference/test-filter.xdef,/nop/schema/xdsl.xdef + + """, "/test/reference/test-filter.xdef"); + doTest(""" + + + /test/reference/test-filter.xdef,/nop/schema/xdsl.xdef + + + """, "/nop/schema/xdsl.xdef"); + doTest(""" + + t-filter.xdef, /nop/schema/xdsl.xdef + ]]> + + """, "/test/reference/test-filter.xdef"); + doTest(""" + + /xdsl.xdef + ]]> + + """, "/nop/schema/xdsl.xdef"); + } + /** 通过在 text 中插入 <caret> 代表光标位置 */ private void doTest(String text, String expected) { configureByXLangText(text); diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index b5c91b987..cb5c59d88 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -16,6 +16,8 @@ --> + + + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index edf3a7784..6c919be85 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -1,6 +1,52 @@ - + + + + + + + + + { + if (filter) { + query.addFilter(filter(query,svcCtx)); + } + + if (orderBy) { + query.addOrderByNode(orderBy(svcCtx)); + } + + if (prepareQuery != null) { + prepareQuery(query, svcCtx); + } + }, + prepareDelete: (entity, ctx) => { + if (prepareDelete) { + prepareDelete(entity, svcCtx); + } + } + }, null, svcCtx); + + if (method == 'findListByQuery') { + return ormTemplate.findListByQuery(query,mapper); + } + return ormTemplate.findFirstByQuery(query, mapper); + ]]> + + -- Gitee From db7e8013f6bf347705acf6222df1f88e6c90c056 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 26 Jun 2025 11:05:48 +0800 Subject: [PATCH 21/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20xdef=20=E4=B8=AD=20enum=E3=80=81dict=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=9A=84=20class=20=E5=92=8C=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reference/XLangElementReference.java | 12 ++- .../reference/XLangReferenceProvider.java | 80 +++++++++++++++++-- .../reference/XLangVfsFileReference.java | 6 +- .../plugin/reference/XLangXDefReference.java | 8 ++ .../reference/TestXLangReferenceProvider.java | 61 +++++++------- 5 files changed, 127 insertions(+), 40 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java index 15a29bd6e..a5a38c90c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java @@ -2,6 +2,7 @@ package io.nop.idea.plugin.reference; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiManager; import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.xml.XmlElement; import org.jetbrains.annotations.NotNull; @@ -14,9 +15,9 @@ import org.jetbrains.annotations.Nullable; * @date 2025-06-23 */ public class XLangElementReference extends PsiReferenceBase implements XLangReference { - private final XmlElement target; + private final PsiElement target; - public XLangElementReference(@NotNull XmlElement element, TextRange rangeInElement, XmlElement target) { + public XLangElementReference(@NotNull XmlElement element, TextRange rangeInElement, PsiElement target) { super(element, rangeInElement); this.target = target; } @@ -25,4 +26,11 @@ public class XLangElementReference extends PsiReferenceBase implemen public @Nullable PsiElement resolve() { return target; } + + @Override + public boolean isReferenceTo(@NotNull PsiElement target) { + // XmlAttributeReference#isReferenceTo + PsiManager manager = getElement().getManager(); + return manager.areElementsEquivalent(target, this.target); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java index 821aac02a..ab4f1bc58 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -10,10 +10,12 @@ import java.util.Objects; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; +import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceProvider; +import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; @@ -34,9 +36,14 @@ import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; +import io.nop.xlang.xdef.parse.XDefTypeDeclParser; import org.jetbrains.annotations.NotNull; import static io.nop.idea.plugin.utils.XmlPsiHelper.isElementType; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_DICT; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_ENUM; +import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_ATTR_PREFIX; +import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_PREFIX_OPTIONS; /** * 针对 XLang 中的 {@link PsiElement 元素} 创建引用 @@ -169,9 +176,9 @@ public class XLangReferenceProvider extends PsiReferenceProvider { return getReferencesByDefault(refElement, attrValue); } - // TODO 对于声明属性,仅对其类型的定义(涉及枚举和字典)做跳转 + // 对于声明属性,仅对其类型的定义(涉及枚举和字典)做引用识别 if (tagInfo.isDefDeclaredAttr(attrName)) { - return PsiReference.EMPTY_ARRAY; + return getReferencesByDefDeclaredAttr(refElement, attrValue); } // 根据属性声明的类型,对属性值做文件/名字引用 @@ -231,7 +238,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, // 从而精确匹配与引用相关的文本内容 int textRangeOffset = refElement.getText().indexOf(refValue); - TextRange textRange = new TextRange(textRangeOffset, refValue.length() + textRangeOffset); + TextRange textRange = new TextRange(0, refValue.length()).shiftRight(textRangeOffset); String stdDomain = refDefType.getStdDomain(); @@ -250,15 +257,76 @@ public class XLangReferenceProvider extends PsiReferenceProvider { return null; } + /** 根据属性的定义识别引用 */ + private PsiReference[] getReferencesByDefDeclaredAttr(XmlElement refElement, String refValue) { + XDefTypeDecl refDefType = new XDefTypeDeclParser().parseFromText(null, refValue); + + // (!~#)?{stdDomain}(:{options})?(={defaultValue})? + String stdDomain = refDefType.getStdDomain(); + String options = refDefType.getOptions(); + Object defaultValue = refDefType.getDefaultValue(); + List defaultAttrNames = refDefType.getDefaultAttrNames(); + + // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, + // 从而精确匹配与引用相关的文本内容 + int textRangeOffset = refElement.getText().indexOf(refValue); + + int stdDomainIndex = refValue.indexOf(stdDomain); + int optionsIndex = options != null ? refValue.indexOf(XDEF_TYPE_PREFIX_OPTIONS + options) + 1 : -1; + int defaultValueIndex = defaultValue != null ? refValue.indexOf("=" + defaultValue) + 1 : -1; + int defaultAttrNamesIndex = defaultAttrNames != null ? refValue.indexOf('=' + XDEF_TYPE_ATTR_PREFIX) + + 1 + + XDEF_TYPE_ATTR_PREFIX.length() : -1; + + List refs = new ArrayList<>(); + + // TODO 引用类型定义 + TextRange textRange = new TextRange(0, stdDomain.length()).shiftRight(textRangeOffset + stdDomainIndex); + refs.add(new XLangElementReference(refElement, textRange, null)); + + if (optionsIndex > 0) { + textRange = new TextRange(0, options.length()).shiftRight(textRangeOffset + optionsIndex); + + if (STD_DOMAIN_ENUM.equals(stdDomain)) { + if (StringHelper.isValidClassName(options)) { + PsiElement target = JavaPsiFacade.getInstance(refElement.getProject()) + .findClass(options, + GlobalSearchScope.allScope(refElement.getProject())); + + refs.add(new XLangElementReference(refElement, textRange, target)); + } + } else if (STD_DOMAIN_DICT.equals(stdDomain)) { + List files = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, + "/dict/" + options + ".dict.yaml"); + + for (PsiFile file : files) { + refs.add(new XLangVfsFileReference(refElement, textRange, file)); + } + } + } + + // TODO 引用字典/枚举值 + if (defaultValueIndex > 0) { + textRange = new TextRange(0, defaultValue.toString().length()).shiftRight(textRangeOffset + + defaultValueIndex); + refs.add(new XLangElementReference(refElement, textRange, null)); + } + + // TODO 引用节点属性 + + return refs.toArray(PsiReference[]::new); + } + /** 对文本做默认的引用识别 */ private PsiReference[] getReferencesByDefault(XmlElement refElement, String refValue) { if (!refValue.endsWith(".xdef")) { return PsiReference.EMPTY_ARRAY; } - // Note: XmlAttributeValue 的文本范围是包含引号的 - int textRangeOffset = refElement instanceof XmlAttributeValue ? 1 : 0; - TextRange textRange = new TextRange(textRangeOffset, refValue.length() + textRangeOffset); + // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, + // 从而精确匹配与引用相关的文本内容 + int textRangeOffset = refElement.getText().indexOf(refValue); + TextRange textRange = new TextRange(0, refValue.length()).shiftRight(textRangeOffset); return getReferencesByVfsPath(refElement, refValue, textRange); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java index 10917710c..538cc2f74 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java @@ -3,6 +3,7 @@ package io.nop.idea.plugin.reference; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; @@ -44,7 +45,7 @@ public class XLangVfsFileReference extends PsiReferenceBase implemen public @Nullable PsiElement resolve() { PsiElement result = XmlPsiHelper.findFirstElement(file, (element) -> element instanceof XmlTag); - return result == null ? this.file : result; + return result == null ? file : result; } /** 得到补全建议元素列表,可以为字符串或 {@link PsiElement} */ @@ -56,6 +57,7 @@ public class XLangVfsFileReference extends PsiReferenceBase implemen @Override public boolean isReferenceTo(@NotNull PsiElement target) { // XmlAttributeReference#isReferenceTo - return target instanceof PsiFile && ((PsiFile) target).getVirtualFile().getPath().endsWith("/_vfs" + file); + PsiManager manager = getElement().getManager(); + return manager.areElementsEquivalent(target, this.file); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java index d991d88ca..b10c0f8d5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java @@ -2,6 +2,7 @@ package io.nop.idea.plugin.reference; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiManager; import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.xml.XmlElement; import org.jetbrains.annotations.NotNull; @@ -25,4 +26,11 @@ public class XLangXDefReference extends PsiReferenceBase implements public @Nullable PsiElement resolve() { return target; } + + @Override + public boolean isReferenceTo(@NotNull PsiElement target) { + // XmlAttributeReference#isReferenceTo + PsiManager manager = getElement().getManager(); + return manager.areElementsEquivalent(target, this.target); + } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index 2037ac850..356e480c5 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -225,7 +225,9 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // doTest(""" // // """, "/test/reference/a.xlib"); -// + } + + public void testGetReferencesFromXmlAttributeType() { // // TODO 声明属性将 引用 属性的类型定义 // doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", // "xdef:ref=\"xdef-ref\""), ""); @@ -233,10 +235,8 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // "-name\""), ""); // doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", "x:schema=\"v-path\""), // ""); - } - public void testGetReferencesFromXmlAttributeType() { - // TODO 字典/枚举的 options 引用 + // 字典/枚举的 options 引用 doTest(""" """, "/dict/test/doc/child-type.dict.yaml"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // - "io.nop.xlang.xdef.XDefOverride"); - - // TODO 字典/枚举的默认值引用 - doTest(""" - - - - """, ""); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // - ""); - - // TODO 缺省属性值中 @attr: 引用 - doTest(""" - - - - """, ""); +// // TODO 如何处理单元测试中的 class 枚举引用? +// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( +// "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", +// "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // +// "io.nop.xlang.xdef.XDefOverride"); +// +// // TODO 字典/枚举的默认值引用 +// doTest(""" +// +// +// +// """, ""); +// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( +// "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", +// "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // +// ""); +// +// // TODO 缺省属性值中 @attr: 引用 +// doTest(""" +// +// +// +// """, ""); } public void testGetReferencesFromXmlAttribute() { -- Gitee From 4f4b8a8e9ce881d93961092bac71731389be847b Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 26 Jun 2025 16:36:40 +0800 Subject: [PATCH 22/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=B0=86=20vfs=20=E8=B5=84=E6=BA=90=E5=A4=8D=E5=88=B6=E5=88=B0?= =?UTF-8?q?=20Project=20=E4=B8=AD=EF=BC=8C=E4=BB=A5=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=BC=95=E7=94=A8=E6=97=B6=E5=8F=91=E7=94=9F?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../idea/plugin/BaseXLangPluginTestCase.java | 94 +++++++++++++------ .../reference/TestXLangReferenceProvider.java | 58 +++++------- .../_vfs/test/java/XDefOverride.java | 57 +++++++++++ 3 files changed, 142 insertions(+), 67 deletions(-) create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/java/XDefOverride.java diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 8ae369190..3594d69b9 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -1,5 +1,13 @@ package io.nop.idea.plugin; +import java.io.File; +import java.io.FileInputStream; +import java.nio.charset.Charset; +import java.nio.file.FileVisitResult; +import java.util.ArrayList; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + import com.intellij.codeInsight.TargetElementUtil; import com.intellij.codeInsight.documentation.DocumentationManager; import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; @@ -10,20 +18,15 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; -import io.nop.api.core.ApiConfigs; -import io.nop.api.core.config.AppConfig; import io.nop.commons.lang.impl.Cancellable; -import io.nop.core.dict.DictProvider; -import io.nop.core.initialize.ICoreInitializer; -import io.nop.core.initialize.impl.ReflectionHelperMethodInitializer; -import io.nop.core.initialize.impl.VirtualFileSystemInitializer; +import io.nop.commons.util.FileHelper; +import io.nop.commons.util.IoHelper; import io.nop.core.resource.IResource; import io.nop.core.resource.ResourceHelper; -import io.nop.core.resource.VirtualFileSystem; +import io.nop.core.resource.impl.ClassPathResource; import io.nop.idea.plugin.lang.XLangFileType; import io.nop.idea.plugin.reference.XLangReference; -import io.nop.idea.plugin.resource.ProjectEnv; -import io.nop.xlang.initialize.XLangCoreInitializer; +import io.nop.idea.plugin.services.NopAppListener; /** * @author flytreeleft @@ -45,26 +48,13 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, "xdef"); FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, XLANG_EXT); - }); - - // 初始化 XLang 环境:由于测试资源均在 classpath 中,故而,需采用默认的 ICoreInitializer 进行初始化, - // 而不能通过 NopAppListener 初始化 - ProjectEnv.withProject(getProject(), () -> { - AppConfig.getConfigProvider().updateConfigValue(ApiConfigs.CFG_DEBUG, false); - - ICoreInitializer[] initializers = new ICoreInitializer[] { - new XLangCoreInitializer(), - new VirtualFileSystemInitializer(), - new ReflectionHelperMethodInitializer(), - }; - for (ICoreInitializer initializer : initializers) { - initializer.initialize(); - cleanup.appendOnCancelTask(initializer::destroy); - } - cleanup.append(DictProvider.registerLoader()); + new NopAppListener().appFrameCreated(new ArrayList<>()); - return null; + // Note: 提前将被引用的 vfs 资源添加到 Project 中 + addAllNopXDefsToProject(); + addVfsResourcesToProject("/nop/core/xlib/meta-gen.xlib"); + addAllTestVfsResourcesToProject(); }); } @@ -78,21 +68,65 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur myFixture.configureByText("unit." + XLANG_EXT, text); } + protected void addAllNopXDefsToProject() { + String jarPath = getClass().getResource("/_vfs/nop/schema/xdef.xdef") + .getPath() + .replaceAll("^file:", "") + .replaceAll("!.+$", ""); + + try (ZipInputStream zip = new ZipInputStream(new FileInputStream(jarPath))) { + for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + if (!entry.getName().endsWith(".xdef")) { + continue; + } + + String path = entry.getName().replaceAll("^_vfs/", "/"); + String text = IoHelper.readText(zip, Charset.defaultCharset().name()); + + addVfsResourceToProject('/' + path, text); + } + } catch (Exception ignore) { + } + } + + /** 将 vfs 测试资源全部复制到 Project 中 */ + protected void addAllTestVfsResourcesToProject() { + File vfsDir = new File(getClass().getResource("/_vfs").getFile()); + + FileHelper.walk(vfsDir, (file) -> { + if (file.isFile()) { + String path = FileHelper.getRelativePath(vfsDir, file); + String text = FileHelper.readText(file, Charset.defaultCharset().name()); + + addVfsResourceToProject('/' + path, text); + } + return FileVisitResult.CONTINUE; + }); + } + /** * 将测试环境中的 vfs 资源添加到 Project 中 *

- * 在需要做文件跳转时,需要将目标文件提前加入 Project 以确保目标文件已存在 + * 在需要做文件引用时,需要将目标文件提前加入 Project 以确保目标文件已存在 */ protected void addVfsResourcesToProject(String... resources) { for (String resource : resources) { String text = readVfsResource(resource); - myFixture.addFileToProject("_vfs" + resource, text); + addVfsResourceToProject(resource, text); + } + } + + protected void addVfsResourceToProject(String path, String text) { + if (path.endsWith(".java")) { + myFixture.addClass(text); + } else { + myFixture.addFileToProject("_vfs" + path, text); } } protected String readVfsResource(String resource) { - IResource res = VirtualFileSystem.instance().getResource(resource); + IResource res = new ClassPathResource("classpath:_vfs" + resource); return ResourceHelper.readText(res); } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index 356e480c5..d1af11ca6 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -1,10 +1,10 @@ package io.nop.idea.plugin.reference; +import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlTag; import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -15,29 +15,6 @@ import io.nop.idea.plugin.utils.XmlPsiHelper; */ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { - @Override - protected void setUp() throws Exception { - super.setUp(); - - // Note: 提前将被引用的文件添加到 Project 中 - addVfsResourcesToProject("/nop/schema/xdef.xdef", - "/nop/schema/xdsl.xdef", - "/nop/schema/xmeta.xdef", - "/nop/schema/xui/xview.xdef", - "/nop/schema/xui/store.xdef", - "/nop/schema/xui/import.xdef", - "/nop/core/xlib/meta-gen.xlib", - "/nop/schema/schema/obj-schema.xdef", - "/nop/schema/schema/schema-node.xdef", - "/dict/test/doc/child-type.dict.yaml", - "/test/doc/example.xdef", - "/test/reference/a.xmeta", - "/test/reference/b.xmeta", - "/test/reference/a.xlib", - "/test/reference/default.xform", - "/test/reference/test-filter.xdef"); - } - public void testGetReferencesFromXmlAttributeValue() { // 对 v-path 属性值的引用 // - x:schema=v-path @@ -235,16 +212,15 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // "-name\""), ""); // doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", "x:schema=\"v-path\""), // ""); - - // 字典/枚举的 options 引用 - doTest(""" - - - - """, "/dict/test/doc/child-type.dict.yaml"); -// // TODO 如何处理单元测试中的 class 枚举引用? +// +// // 字典/枚举的 options 引用 +// doTest(""" +// +// +// +// """, "/dict/test/doc/child-type.dict.yaml"); // doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( // "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", // "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // @@ -268,7 +244,14 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { // -// +// +// +// """, ""); +// doTest(""" +// +// // // """, ""); } @@ -401,8 +384,6 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); } // else if (ref instanceof XLangElementReference || ref instanceof XLangXDefReference) { - assertInstanceOf(target, XmlElement.class); - if (target instanceof XmlTag tag) { assertEquals(expected, tag.getName()); } // @@ -410,6 +391,9 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); assertEquals(expected, tag.getName() + "#" + attr.getName() + "=" + attr.getValue()); + } // + else if (target instanceof PsiClass cls) { + assertEquals(expected, cls.getQualifiedName()); } else { fail("Unknown target " + target.getClass()); } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefOverride.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefOverride.java new file mode 100644 index 000000000..d8da5028b --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefOverride.java @@ -0,0 +1,57 @@ +package io.nop.xlang.xdef; + +import java.util.HashMap; +import java.util.Map; + +import io.nop.api.core.annotations.core.Description; +import io.nop.api.core.annotations.core.Locale; +import io.nop.api.core.annotations.core.Option; +import io.nop.api.core.annotations.core.StaticFactoryMethod; + +@Locale("zh-CN") +public enum XDefOverride { + @Option("remove") @Description("删除基类中的节点") REMOVE("remove"), + + @Option("replace") @Description("完全覆盖原有节点") REPLACE("replace"), + + @Option("prepend") @Description("合并属性,并前插子节点") PREPEND("prepend"), + + @Option("append") @Description("合并属性,并后插子节点") APPEND("append"), + + @Option("merge") @Description("合并属性,并按照标签名合并子节点") MERGE("merge"), + + @Option("merge-replace") @Description( + "合并属性,并覆盖子节点。可以通过设置属性为空字符串或者null来达到删除属性的效果") MERGE_REPLACE( + "merge-replace"), + + @Option("bounded-merge") @Description("只保留派生节点中定义过的子节点") BOUNDED_MERGE("bounded-merge"), + + @Option("merge-super") @Description("合并属性,嵌入super") MERGE_SUPER("merge-super"); + + private String text; + + XDefOverride(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + public String toString() { + return text; + } + + private static final Map textMap = new HashMap(); + + static { + for (XDefOverride value : XDefOverride.values()) { + textMap.put(value.getText(), value); + } + } + + @StaticFactoryMethod + public static XDefOverride fromText(String text) { + return textMap.get(text); + } +} -- Gitee From 556a58e604aa2d8c1b386b83681093c3062df47d Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 27 Jun 2025 13:03:39 +0800 Subject: [PATCH 23/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E6=95=B0=E6=8D=AE=E5=9F=9F=E5=90=8D=E5=AD=97?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E5=85=B6=20class=20=E5=BC=95=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=8F=AF=E8=AF=86=E5=88=AB=E5=AD=97=E5=85=B8?= =?UTF-8?q?/=E6=9E=9A=E4=B8=BE=E7=B1=BB=E5=9E=8B=E7=9A=84=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC=E6=89=80=E5=AF=B9=E5=BA=94=E7=9A=84=E5=AD=97?= =?UTF-8?q?=E5=85=B8=E5=80=BC=E5=92=8C=E6=9E=9A=E4=B8=BE=E9=A1=B9=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nop-idea-plugin/build.gradle.kts | 2 +- .../reference/XLangReferenceProvider.java | 47 ++++- .../plugin/resource/EnumDictOptionBean.java | 20 ++ .../plugin/resource/ProjectDictProvider.java | 99 ++++----- .../nop/idea/plugin/utils/PsiClassHelper.java | 191 ++++++++++++++++++ .../src/main/resources/META-INF/plugin.xml | 1 + .../reference/TestXLangReferenceProvider.java | 93 +++++---- .../_vfs/test/java/IStdDomainHandler.java | 6 + .../test/java/VueNodeStdDomainHandler.java | 11 + .../_vfs/test/java/XDefConstants.java | 5 + .../_vfs/test/java/XJsonDomainHandler.java | 11 + 11 files changed, 392 insertions(+), 94 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictOptionBean.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/java/IStdDomainHandler.java create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/java/VueNodeStdDomainHandler.java create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/java/XDefConstants.java create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java diff --git a/nop-idea-plugin/build.gradle.kts b/nop-idea-plugin/build.gradle.kts index 0fc06615f..dad69cb89 100644 --- a/nop-idea-plugin/build.gradle.kts +++ b/nop-idea-plugin/build.gradle.kts @@ -23,7 +23,7 @@ intellij { //version.set("2022.3") type.set("IC") // Target IDE Platform - plugins.set(listOf("java")) + plugins.set(listOf("java", "org.jetbrains.plugins.yaml")) } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java index ab4f1bc58..b30e2a95e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -11,10 +11,12 @@ import java.util.Objects; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceProvider; +import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; @@ -23,12 +25,17 @@ import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.util.ProcessingContext; +import io.nop.api.core.beans.DictBean; +import io.nop.api.core.beans.DictOptionBean; import io.nop.api.core.util.SourceLocation; import io.nop.commons.text.MutableString; import io.nop.commons.text.tokenizer.TextScanner; import io.nop.commons.util.StringHelper; +import io.nop.core.dict.DictProvider; import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.resource.EnumDictOptionBean; import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.idea.plugin.utils.PsiClassHelper; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.utils.XmlTagInfo; @@ -38,6 +45,7 @@ import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.parse.XDefTypeDeclParser; import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.psi.YAMLKeyValue; import static io.nop.idea.plugin.utils.XmlPsiHelper.isElementType; import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_DICT; @@ -280,9 +288,13 @@ public class XLangReferenceProvider extends PsiReferenceProvider { List refs = new ArrayList<>(); - // TODO 引用类型定义 + // 引用数据域的类型定义 TextRange textRange = new TextRange(0, stdDomain.length()).shiftRight(textRangeOffset + stdDomainIndex); - refs.add(new XLangElementReference(refElement, textRange, null)); + List stdDomainClsList = PsiClassHelper.findStdDomainHandlers(refElement.getProject()) + .getOrDefault(stdDomain, new ArrayList<>()); + for (PsiClass cls : stdDomainClsList) { + refs.add(new XLangElementReference(refElement, textRange, cls)); + } if (optionsIndex > 0) { textRange = new TextRange(0, options.length()).shiftRight(textRangeOffset + optionsIndex); @@ -305,14 +317,41 @@ public class XLangReferenceProvider extends PsiReferenceProvider { } } - // TODO 引用字典/枚举值 + // 引用字典/枚举值 if (defaultValueIndex > 0) { textRange = new TextRange(0, defaultValue.toString().length()).shiftRight(textRangeOffset + defaultValueIndex); - refs.add(new XLangElementReference(refElement, textRange, null)); + + DictBean dictBean = DictProvider.instance().getDict(null, options, null, null); + DictOptionBean dictOpt = dictBean != null ? dictBean.getOptionByValue(defaultValue) : null; + + if (dictOpt instanceof EnumDictOptionBean opt) { + refs.add(new XLangElementReference(refElement, textRange, opt.target)); + } else if (STD_DOMAIN_DICT.equals(stdDomain)) { + List files = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, + "/dict/" + options + ".dict.yaml"); + + for (PsiFile file : files) { + PsiElement target = XmlPsiHelper.findFirstElement(file, (element) -> { + if (element instanceof LeafPsiElement value // + && defaultValue.equals(value.getText()) // + ) { + PsiElement parent = PsiTreeUtil.getParentOfType(element, YAMLKeyValue.class); + PsiElement key = parent != null ? parent.getFirstChild() : null; + + return key != null && "value".equals(key.getText()); + } + return false; + }); + refs.add(new XLangElementReference(refElement, textRange, target)); + } + } } // TODO 引用节点属性 + if (defaultAttrNamesIndex > 0) { + // + } return refs.toArray(PsiReference[]::new); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictOptionBean.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictOptionBean.java new file mode 100644 index 000000000..68a6b82ce --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictOptionBean.java @@ -0,0 +1,20 @@ +package io.nop.idea.plugin.resource; + +import com.intellij.psi.PsiField; +import io.nop.api.core.beans.DictOptionBean; + +/** + * 根据枚举类所生成的 {@link DictOptionBean} + *

+ * 用于得到字典项的关联元素 + * + * @author flytreeleft + * @date 2025-06-27 + */ +public class EnumDictOptionBean extends DictOptionBean { + public final PsiField target; + + public EnumDictOptionBean(PsiField target) { + this.target = target; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java index 05c83eb4d..de49fc1d8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java @@ -7,22 +7,20 @@ */ package io.nop.idea.plugin.resource; +import java.util.ArrayList; +import java.util.List; + import com.intellij.lang.jvm.JvmEnumField; -import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute; -import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue; import com.intellij.openapi.project.Project; -import com.intellij.psi.JavaPsiFacade; -import com.intellij.psi.PsiAnnotation; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiField; -import com.intellij.psi.search.GlobalSearchScope; import io.nop.api.core.annotations.core.Description; import io.nop.api.core.annotations.core.Label; import io.nop.api.core.annotations.core.Option; import io.nop.api.core.beans.DictBean; import io.nop.api.core.beans.DictOptionBean; -import io.nop.api.core.convert.ConvertHelper; import io.nop.commons.cache.ICache; +import io.nop.commons.util.StringHelper; import io.nop.core.context.IEvalContext; import io.nop.core.dict.DictModel; import io.nop.core.dict.DictModelParser; @@ -32,11 +30,10 @@ import io.nop.core.resource.IResource; import io.nop.core.resource.VirtualFileSystem; import io.nop.core.resource.component.ResourceComponentManager; import io.nop.idea.plugin.services.NopProjectService; - -import java.util.ArrayList; -import java.util.List; +import io.nop.idea.plugin.utils.PsiClassHelper; public class ProjectDictProvider implements IDictProvider { + @Override public DictBean getDict(String locale, String dictName, ICache cache, IEvalContext context) { return NopProjectService.get().getDict(dictName); @@ -63,68 +60,60 @@ public class ProjectDictProvider implements IDictProvider { if (dictName.indexOf('/') > 0) { String path = "/dict/" + dictName + ".dict.yaml"; IResource resource = VirtualFileSystem.instance().getResource(path); + if (resource.exists()) { ResourceComponentManager.instance().traceDepends(resource.getPath()); + return new DictModelParser().parseFromResource(resource); } - } else if (dictName.indexOf('.') > 0) { - // load dict from class - PsiClass clazz = JavaPsiFacade.getInstance(project).findClass(dictName, GlobalSearchScope.allScope(project)); - if (clazz != null) { - DictBean dict = new DictBean(); - List options = new ArrayList<>(); - for (PsiField field : clazz.getFields()) { - if (!(field instanceof JvmEnumField)) - continue; - - DictOptionBean option = buildOption(field); - options.add(option); + } + // 从枚举类中得到字典信息 + else if (dictName.indexOf('.') > 0 && StringHelper.isValidClassName(dictName)) { + PsiClass clazz = PsiClassHelper.findClass(project, dictName); + if (clazz == null) { + return null; + } + + List options = new ArrayList<>(); + for (PsiField field : clazz.getFields()) { + if (!(field instanceof JvmEnumField)) { + continue; } - dict.setOptions(options); - DictModel ret = new DictModel(); - ret.setDictBean(dict); - -// final PsiConstantEvaluationHelper evaluationHelper = -// JavaPsiFacade.getInstance(ProjectEnv.currentProject()).getConstantEvaluationHelper(); -// final Set enumValues = new HashSet<>(); -// for (PsiField enumConstant : clazz.getFields()) { -// if (enumConstant instanceof JvmEnumField) { -// enumValues.add(evaluationHelper.computeConstantExpression(enumConstant.getInitializer())); -// } -// } - return ret; + + DictOptionBean option = buildOption(field); + options.add(option); } + + DictBean dict = new DictBean(); + dict.setOptions(options); + + DictModel ret = new DictModel(); + ret.setDictBean(dict); + + return ret; } return null; } private static DictOptionBean buildOption(PsiField field) { - DictOptionBean option = new DictOptionBean(); - String value = getAnnotationValue(field, Option.class.getName()); - if (value == null) + DictOptionBean option = new EnumDictOptionBean(field); + + String value = (String) PsiClassHelper.getAnnotationValue(field, Option.class.getName()); + if (value == null) { value = field.getName(); + } + option.setValue(value); - String label = getAnnotationValue(field, Label.class.getName()); - if (label == null) + String label = (String) PsiClassHelper.getAnnotationValue(field, Label.class.getName()); + if (label == null) { label = value; - String description = getAnnotationValue(field, Description.class.getName()); - option.setValue(value); + } option.setLabel(label); + + String description = (String) PsiClassHelper.getAnnotationValue(field, Description.class.getName()); option.setDescription(description); - return option; - } - private static String getAnnotationValue(PsiField field, String annName) { - PsiAnnotation ann = field.getAnnotation(annName); - if (ann == null) - return null; - for (JvmAnnotationAttribute attr : ann.getAttributes()) { - if (attr.getAttributeName().equals("value")) { - if (attr.getAttributeValue() instanceof JvmAnnotationConstantValue) - return ConvertHelper.toString(((JvmAnnotationConstantValue) attr.getAttributeValue()).getConstantValue()); - } - } - return null; + return option; } -} \ No newline at end of file +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java new file mode 100644 index 000000000..371d4a24e --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -0,0 +1,191 @@ +package io.nop.idea.plugin.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.intellij.openapi.project.Project; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiConstantEvaluationHelper; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementFactory; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiReturnStatement; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.searches.ClassInheritorsSearch; +import com.intellij.util.EmptyQuery; +import com.intellij.util.Query; +import io.nop.commons.util.StringHelper; +import io.nop.xlang.xdef.IStdDomainHandler; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-26 + */ +public class PsiClassHelper { + + /** + * 查找项目中 {@link io.nop.xlang.xdef.IStdDomainHandler IStdDomainHandler} 的实现类, + * 并以其{@link io.nop.xlang.xdef.IStdDomainHandler#getName() 名字}为 Map Key + */ + public static Map> findStdDomainHandlers(Project project) { + Map> map = new HashMap<>(); + + Query query = findInheritors(project, IStdDomainHandler.class.getName()); + query.filtering((cls) -> !cls.isInterface() + && !cls.isEnum() + && !cls.isAnnotationType() + && !cls.hasModifierProperty(PsiModifier.ABSTRACT) // + ) // + .forEach((cls) -> { + Object name = getMethodReturnConstantValue(cls, "getName"); + + if (name != null) { + map.computeIfAbsent(name.toString(), (k) -> new ArrayList<>()).add(cls); + } + }); + + return map; + } + + public static PsiClass findClass(Project project, String clsName) { + return JavaPsiFacade.getInstance(project).findClass(clsName, GlobalSearchScope.allScope(project)); + } + + /** 查找指定类的继承类 */ + public static @NotNull Query findInheritors(Project project, String clsName) { + PsiClass cls = findClass(project, clsName); + if (cls == null) { + return EmptyQuery.getEmptyQuery(); + } + + return ClassInheritorsSearch.search(cls, true); + } + + /** 获取指定方法返回的常量值 */ + public static Object getMethodReturnConstantValue(PsiClass cls, String methodName) { + PsiMethod[] methods = cls.findMethodsByName(methodName, true); + if (methods.length == 0) { + return null; + } + + PsiMethod method = methods[0]; + ReturnStatementAnalyzer analyzer = new ReturnStatementAnalyzer(); + method.accept(analyzer); + + return analyzer.getReturnValue(); + } + + /** + * 获取 {@link PsiField} 上指定注解的 value 值 + */ + public static Object getAnnotationValue(PsiField field, String annName) { + return getAnnotationValue(field, annName, null); + } + + /** + * 获取 {@link PsiField} 上指定注解的属性值 + * + * @param annAttrName + * 若为 null,则取注解的 value 值 + */ + public static Object getAnnotationValue(PsiField field, String annName, String annAttrName) { + PsiAnnotation ann = field.getAnnotation(annName); + if (ann == null) { + // Note: 单元测试中只能根据 simple class name 得到 + ann = field.getAnnotation(StringHelper.simpleClassName(annName)); + } + + if (ann == null) { + return null; + } + + for (PsiNameValuePair pair : ann.getParameterList().getAttributes()) { + String name = pair.getName(); + + if (Objects.equals(name, annAttrName)) { + return computeConstantExpression(pair.getValue()); + } + } + return null; + } + + /** + * 计算常量表达式: + *
+     * - 1 + 2 * 3
+     * - "IDEA" + " Plugin"
+     * - (2 + 3) * (4 - 1)
+     * - (double) 10 / 3
+     * - 5 > 3 ? "yes" : "no"
+     * 
+ */ + public static Object computeConstantExpression(Project project, String expression) { + PsiExpression expr = PsiElementFactory.getInstance(project).createExpressionFromText(expression, null); + + return computeConstantExpression(expr); + } + + /** 计算常量表达式 */ + public static Object computeConstantExpression(PsiElement expression) { + if (expression == null) { + return null; + } + + PsiConstantEvaluationHelper helper = JavaPsiFacade.getInstance(expression.getProject()) + .getConstantEvaluationHelper(); + return helper.computeConstantExpression(expression); + } + + private static class ReturnStatementAnalyzer extends JavaRecursiveElementVisitor { + private PsiExpression returnExpr; + + @Override + public void visitReturnStatement(@NotNull PsiReturnStatement statement) { + // 只捕获第一个返回语句 + if (returnExpr == null) { + returnExpr = statement.getReturnValue(); + } + } + + public Object getReturnValue() { + return evaluateExpression(returnExpr); + } + + private Object evaluateExpression(PsiExpression expression) { + // 处理字面量表达式 + if (expression instanceof PsiLiteralExpression) { + return ((PsiLiteralExpression) expression).getValue(); + } + // 处理引用表达式(字段引用) + else if (expression instanceof PsiReferenceExpression) { + PsiElement resolved = ((PsiReferenceExpression) expression).resolve(); + + // 解析到常量字段(包含接口上的常量) + if (resolved instanceof PsiField field // + && field.hasModifierProperty(PsiModifier.STATIC) // + && field.hasModifierProperty(PsiModifier.FINAL) // + ) { + // 递归解析字段的初始化表达式 + PsiExpression initializer = field.getInitializer(); + + return evaluateExpression(initializer); + } + } + + return computeConstantExpression(expression); + } + } +} diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 6ebfb3d22..0f19838ae 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -16,6 +16,7 @@ com.intellij.modules.lang com.intellij.java + org.jetbrains.plugins.yaml diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index d1af11ca6..37328a357 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -2,7 +2,10 @@ package io.nop.idea.plugin.reference; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiPlainText; import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlTag; @@ -205,39 +208,47 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { } public void testGetReferencesFromXmlAttributeType() { -// // TODO 声明属性将 引用 属性的类型定义 -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", -// "xdef:ref=\"xdef-ref\""), ""); -// doTest(readVfsResource("/nop/schema/xdef.xdef").replace("-name\""), ""); -// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", "x:schema=\"v-path\""), -// ""); -// -// // 字典/枚举的 options 引用 -// doTest(""" -// -// -// -// """, "/dict/test/doc/child-type.dict.yaml"); -// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( -// "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", -// "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // -// "io.nop.xlang.xdef.XDefOverride"); -// -// // TODO 字典/枚举的默认值引用 -// doTest(""" -// -// -// -// """, ""); -// doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( -// "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", -// "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // -// ""); + // 声明属性将 引用 属性的类型定义 + doTest(""" + + + + """, "io.nop.xui.initialize.VueNodeStdDomainHandler"); + doTest(""" + + + + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + + // 字典/枚举的 options 引用 + doTest(""" + + + + """, "/dict/test/doc/child-type.dict.yaml"); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + "io.nop.xlang.xdef.XDefOverride"); + + // 字典/枚举的默认值引用 + doTest(""" + + + + """, "/dict/test/doc/child-type.dict.yaml#leaf"); + doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + "io.nop.xlang.xdef.XDefOverride#MERGE"); // // // TODO 缺省属性值中 @attr: 引用 // doTest(""" @@ -394,7 +405,21 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { } // else if (target instanceof PsiClass cls) { assertEquals(expected, cls.getQualifiedName()); - } else { + } // + else if (target instanceof PsiField field) { + assertEquals(expected, field.getContainingClass().getQualifiedName() + "#" + field.getName()); + } // + else if (target instanceof PsiPlainText txt) { + String vfsPath = XmlPsiHelper.getNopVfsPath(target); + + assertEquals(expected, vfsPath + ":" + txt.getTextOffset()); + } // + else if (target instanceof LeafPsiElement leaf) { + String vfsPath = XmlPsiHelper.getNopVfsPath(target); + + assertEquals(expected, vfsPath + "#" + leaf.getText()); + } // + else { fail("Unknown target " + target.getClass()); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/IStdDomainHandler.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/IStdDomainHandler.java new file mode 100644 index 000000000..53618d830 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/IStdDomainHandler.java @@ -0,0 +1,6 @@ +package io.nop.xlang.xdef; + +public interface IStdDomainHandler { + + String getName(); +} diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/VueNodeStdDomainHandler.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/VueNodeStdDomainHandler.java new file mode 100644 index 000000000..1b060f3e6 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/VueNodeStdDomainHandler.java @@ -0,0 +1,11 @@ +package io.nop.xui.initialize; + +import io.nop.xlang.xdef.XDefConstants; + +public class VueNodeStdDomainHandler implements IStdDomainHandler { + + @Override + public String getName() { + return XDefConstants.STD_DOMAIN_VUE_NODE; + } +} diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefConstants.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefConstants.java new file mode 100644 index 000000000..28c472b64 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefConstants.java @@ -0,0 +1,5 @@ +package io.nop.xlang.xdef; + +public interface XDefConstants { + String STD_DOMAIN_VUE_NODE = "vue-node"; +} diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java new file mode 100644 index 000000000..6695b2a4a --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java @@ -0,0 +1,11 @@ +package io.nop.xlang.xdef.domain; + +import io.nop.xlang.xdef.IStdDomainHandler; + +public class XJsonDomainHandler implements IStdDomainHandler { + + @Override + public String getName() { + return "xjson"; + } +} -- Gitee From 2c753f71c3606bee0e33448c96c38e4c5e4deadb Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 27 Jun 2025 17:42:23 +0800 Subject: [PATCH 24/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E5=B1=9E=E6=80=A7=E7=B1=BB=E5=9E=8B=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E4=B8=AD=20@attr:=20=E6=89=80=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E8=8A=82=E7=82=B9=E5=B1=9E=E6=80=A7=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E5=AF=B9=20xpl=20=E5=87=BD=E6=95=B0=E7=9A=84=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E3=80=82=E5=90=8C=E6=97=B6=EF=BC=8C=E7=94=B1=E4=BA=8E?= =?UTF-8?q?=E6=9A=82=E6=97=B6=E6=97=A0=E6=B3=95=E8=AF=86=E5=88=AB=20class?= =?UTF-8?q?=20=E5=AD=97=E8=8A=82=E7=A0=81=E4=B8=AD=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=9F=9F=EF=BC=8C=E6=95=85=E8=80=8C=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=BC=95=E7=94=A8=E5=AD=97=E5=85=B8=20core/std-domain?= =?UTF-8?q?=20=E4=B8=AD=E5=AE=9A=E4=B9=89=E7=9A=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=9F=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../link/XLangGotoDeclarationHandler.java | 107 ------------------ .../reference/XLangReferenceProvider.java | 105 +++++++++++------ .../nop/idea/plugin/utils/PsiClassHelper.java | 51 ++++----- .../src/main/resources/META-INF/plugin.xml | 5 +- .../messages/NopPluginBundle.properties | 3 +- .../messages/NopPluginBundle_zh.properties | 3 +- .../idea/plugin/BaseXLangPluginTestCase.java | 12 +- .../reference/TestXLangReferenceProvider.java | 75 +++++++----- .../test/resources/_vfs/test/doc/example.xdef | 4 +- .../_vfs/test/reference/user.view.xml | 4 +- 10 files changed, 165 insertions(+), 204 deletions(-) delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java deleted file mode 100644 index aa3048567..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangGotoDeclarationHandler.java +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ -package io.nop.idea.plugin.link; - -import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandlerBase; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiElement; -import com.intellij.psi.xml.XmlElementType; -import com.intellij.psi.xml.XmlTag; -import io.nop.commons.util.StringHelper; -import io.nop.idea.plugin.resource.ProjectEnv; -import io.nop.idea.plugin.utils.XmlPsiHelper; -import org.jetbrains.annotations.Nullable; - -/*** - * 点击 + Ctrl 能够跳转到引用元素 - */ -public class XLangGotoDeclarationHandler extends GotoDeclarationHandlerBase { - - @Override - public @Nullable PsiElement getGotoDeclarationTarget(@Nullable PsiElement sourceElement, Editor editor) { - PsiElement[] elements = getGotoDeclarationTargets(sourceElement, 0, editor); - return elements.length == 0 ? null : elements[0]; - } - - @Override - public @Nullable PsiElement[] getGotoDeclarationTargets( - @Nullable PsiElement sourceElement, int offset, Editor editor - ) { - Project project = editor.getProject(); - - return ProjectEnv.withProject(project, () -> { - PsiElement element = sourceElement != null ? sourceElement.getParent() : null; - - if (XmlPsiHelper.isElementType(element, XmlElementType.XML_TAG)) { - return getGotoDeclarationTargetsForXmlTag(project, element); - } // - else if (XmlPsiHelper.isElementType(element, XmlElementType.XML_ATTRIBUTE_VALUE)) { - return getGotoDeclarationTargetsForXmlAttributeValue(project, element, offset); - } // - else if (XmlPsiHelper.isElementType(element, XmlElementType.XML_TEXT)) { - return getGotoDeclarationTargetsForXmlText(project, element); - } - - return null; - }); - } - - /** 获取可从 xml 标签上跳转的元素(文件路径、节点引用、xpl 函数等) */ - private PsiElement[] getGotoDeclarationTargetsForXmlTag(Project project, PsiElement element) { - XmlTag tag = (XmlTag) element; - String tagName = tag.getName(); - - if (!isCustomTag(tagName)) { - return null; - } - return XmlPsiHelper.findXplTag(project, tag); - } - - /** 获取可从 xml 属性值中跳转的元素(文件路径、节点引用等) */ - private PsiElement[] getGotoDeclarationTargetsForXmlAttributeValue( - Project project, PsiElement element, int cursorOffset - ) { - return null; - } - - /** 获取可从 xml 文本中跳转的元素(文件路径、节点引用等) */ - private PsiElement[] getGotoDeclarationTargetsForXmlText(Project project, PsiElement element) { - if (!(element.getParent() instanceof XmlTag parent)) { - return null; - } - - String text = element.getText().trim(); - if ((text.indexOf('.') > 0 || text.indexOf('/') > 0) // - && StringHelper.isValidFilePath(text) // - ) { - String path = XmlPsiHelper.getNopVfsAbsolutePath(text, parent); - - return XmlPsiHelper.findPsiFiles(project, path); - } - - return null; - } - - private boolean isCustomTag(String tagName) { - int pos = tagName.indexOf(':'); - if (pos <= 0) { - return false; - } - - // 内置的名字空间 - String ns = tagName.substring(0, pos); - return !ns.equals("x") - && !ns.equals("xdef") - && !ns.equals("xdsl") - && !ns.equals("xpl") - && !ns.equals("c") - && !ns.equals("macro") - && !ns.equals("xmlns"); - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java index b30e2a95e..5cfcd9b97 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java @@ -11,7 +11,6 @@ import java.util.Objects; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.JavaPsiFacade; -import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; @@ -35,7 +34,6 @@ import io.nop.core.dict.DictProvider; import io.nop.idea.plugin.messages.NopPluginBundle; import io.nop.idea.plugin.resource.EnumDictOptionBean; import io.nop.idea.plugin.resource.ProjectEnv; -import io.nop.idea.plugin.utils.PsiClassHelper; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.utils.XmlTagInfo; @@ -112,9 +110,30 @@ public class XLangReferenceProvider extends PsiReferenceProvider { /** 获取 xml 标签对应的引用(节点定义、xpl 函数定义等) */ private PsiReference @NotNull [] getReferencesFromXmlTag(XmlTag tag) { - // TODO xpl 函数的引用 + // TODO xpl 函数的引用:根据导入 xlib 中定义的函数进行识别 + // TODO 引用节点定义 + String tagName = tag.getName(); - return PsiReference.EMPTY_ARRAY; + int pos = tagName.indexOf(':'); + if (pos <= 0) { + return PsiReference.EMPTY_ARRAY; + } + + // 内置的名字空间 + String ns = tagName.substring(0, pos); + + if (ns.equals("x") || ns.equals("xdef") || ns.equals("xdsl") || ns.equals("xpl") // + || ns.equals("c") || ns.equals("macro") || ns.equals("xmlns") // + ) { + return PsiReference.EMPTY_ARRAY; + } + + // Note: 仅对名字做引用识别,忽略名字空间 + TextRange textRange = new TextRange(pos + 1, tagName.length()).shiftRight(1); + + return Arrays.stream(XmlPsiHelper.findXplTag(tag.getProject(), tag)) + .map((xpl) -> new XLangElementReference(tag, textRange, xpl)) + .toArray(PsiReference[]::new); } /** 获取 xml 属性名对应的引用(属性定义) */ @@ -290,11 +309,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { // 引用数据域的类型定义 TextRange textRange = new TextRange(0, stdDomain.length()).shiftRight(textRangeOffset + stdDomainIndex); - List stdDomainClsList = PsiClassHelper.findStdDomainHandlers(refElement.getProject()) - .getOrDefault(stdDomain, new ArrayList<>()); - for (PsiClass cls : stdDomainClsList) { - refs.add(new XLangElementReference(refElement, textRange, cls)); - } + refs.addAll(getReferencesFromDictYaml(refElement, "core/std-domain", stdDomain, textRange)); if (optionsIndex > 0) { textRange = new TextRange(0, options.length()).shiftRight(textRangeOffset + optionsIndex); @@ -328,29 +343,28 @@ public class XLangReferenceProvider extends PsiReferenceProvider { if (dictOpt instanceof EnumDictOptionBean opt) { refs.add(new XLangElementReference(refElement, textRange, opt.target)); } else if (STD_DOMAIN_DICT.equals(stdDomain)) { - List files = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, - "/dict/" + options + ".dict.yaml"); - - for (PsiFile file : files) { - PsiElement target = XmlPsiHelper.findFirstElement(file, (element) -> { - if (element instanceof LeafPsiElement value // - && defaultValue.equals(value.getText()) // - ) { - PsiElement parent = PsiTreeUtil.getParentOfType(element, YAMLKeyValue.class); - PsiElement key = parent != null ? parent.getFirstChild() : null; - - return key != null && "value".equals(key.getText()); - } - return false; - }); - refs.add(new XLangElementReference(refElement, textRange, target)); - } + refs.addAll(getReferencesFromDictYaml(refElement, options, defaultValue, textRange)); } } - // TODO 引用节点属性 + // 引用节点属性 if (defaultAttrNamesIndex > 0) { - // + XmlTag tag = PsiTreeUtil.getParentOfType(refElement, XmlTag.class); + Map rangeNameMap = extractValuesFromCsv(refValue.substring(defaultAttrNamesIndex)); + + rangeNameMap.forEach((range, name) -> { + XmlAttribute attr = tag.getAttribute(name); + + range = range.shiftRight(textRangeOffset + defaultAttrNamesIndex); + if (attr == null) { + String msg = NopPluginBundle.message("xlang.annotation.reference.default-value-ref-attr-not-found", + name); + + refs.add(new XLangNotFoundReference(refElement, range, msg)); + } else { + refs.add(new XLangElementReference(refElement, range, attr)); + } + }); } return refs.toArray(PsiReference[]::new); @@ -374,7 +388,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { private PsiReference[] getReferencesFromVfsPathCsv( XmlElement refElement, String refValue, int textRangeOffset ) { - Map rangePathMap = extractPathsFromCsv(refValue); + Map rangePathMap = extractValuesFromCsv(refValue); List list = new ArrayList<>(rangePathMap.size()); rangePathMap.forEach((textRange, path) -> { @@ -523,7 +537,7 @@ public class XLangReferenceProvider extends PsiReferenceProvider { XmlTag tag = tagInfo.getTag(); XmlAttribute attr = tag.getAttribute(attrValue); if (attr == null) { - String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-unique-attr-no-found", attrValue); + String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-unique-attr-not-found", attrValue); return new PsiReference[] { new XLangNotFoundReference(attrValueElement, textRange, msg) }; @@ -552,7 +566,32 @@ public class XLangReferenceProvider extends PsiReferenceProvider { }; } - private Map extractPathsFromCsv(String csv) { + private List getReferencesFromDictYaml( + XmlElement refElement, String dictPath, Object dictOptionValue, TextRange textRange + ) { + List refs = new ArrayList<>(); + List files = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, "/dict/" + dictPath + ".dict.yaml"); + + for (PsiFile file : files) { + PsiElement target = XmlPsiHelper.findFirstElement(file, (element) -> { + if (element instanceof LeafPsiElement value // + && dictOptionValue.equals(value.getText()) // + ) { + PsiElement parent = PsiTreeUtil.getParentOfType(element, YAMLKeyValue.class); + PsiElement key = parent != null ? parent.getFirstChild() : null; + + return key != null && "value".equals(key.getText()); + } + return false; + }); + + refs.add(new XLangElementReference(refElement, textRange, target)); + } + + return refs; + } + + private Map extractValuesFromCsv(String csv) { Map rangePathMap = new HashMap<>(); TextScanner sc = TextScanner.fromString(null, csv); @@ -563,8 +602,8 @@ public class XLangReferenceProvider extends PsiReferenceProvider { MutableString buf = sc.useBuf(); sc.nextUntil(s -> s.cur == ',' || StringHelper.isSpace(sc.cur), sc::appendToBuf); - String path = buf.toString(); - rangePathMap.put(new TextRange(offset, sc.pos), path); + String value = buf.toString(); + rangePathMap.put(new TextRange(offset, sc.pos), value); sc.next(); sc.skipBlank(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java index 371d4a24e..f74e84284 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -39,7 +39,10 @@ public class PsiClassHelper { /** * 查找项目中 {@link io.nop.xlang.xdef.IStdDomainHandler IStdDomainHandler} 的实现类, * 并以其{@link io.nop.xlang.xdef.IStdDomainHandler#getName() 名字}为 Map Key + * + * @deprecated 只能用于源码分析,不能从 class 字节码中得到方法的返回结果 */ + @Deprecated public static Map> findStdDomainHandlers(Project project) { Map> map = new HashMap<>(); @@ -83,7 +86,7 @@ public class PsiClassHelper { PsiMethod method = methods[0]; ReturnStatementAnalyzer analyzer = new ReturnStatementAnalyzer(); - method.accept(analyzer); + method.getBody().accept(analyzer); return analyzer.getReturnValue(); } @@ -144,6 +147,26 @@ public class PsiClassHelper { return null; } + // 处理字面量表达式 + if (expression instanceof PsiLiteralExpression) { + return ((PsiLiteralExpression) expression).getValue(); + } + // 处理引用表达式(字段引用) + else if (expression instanceof PsiReferenceExpression) { + PsiElement resolved = ((PsiReferenceExpression) expression).resolve(); + + // 解析到常量字段(包含接口上的常量) + if (resolved instanceof PsiField field // + && field.hasModifierProperty(PsiModifier.STATIC) // + && field.hasModifierProperty(PsiModifier.FINAL) // + ) { + // 递归解析字段的初始化表达式 + PsiExpression initializer = field.getInitializer(); + + return computeConstantExpression(initializer); + } + } + PsiConstantEvaluationHelper helper = JavaPsiFacade.getInstance(expression.getProject()) .getConstantEvaluationHelper(); return helper.computeConstantExpression(expression); @@ -161,31 +184,7 @@ public class PsiClassHelper { } public Object getReturnValue() { - return evaluateExpression(returnExpr); - } - - private Object evaluateExpression(PsiExpression expression) { - // 处理字面量表达式 - if (expression instanceof PsiLiteralExpression) { - return ((PsiLiteralExpression) expression).getValue(); - } - // 处理引用表达式(字段引用) - else if (expression instanceof PsiReferenceExpression) { - PsiElement resolved = ((PsiReferenceExpression) expression).resolve(); - - // 解析到常量字段(包含接口上的常量) - if (resolved instanceof PsiField field // - && field.hasModifierProperty(PsiModifier.STATIC) // - && field.hasModifierProperty(PsiModifier.FINAL) // - ) { - // 递归解析字段的初始化表达式 - PsiExpression initializer = field.getInitializer(); - - return evaluateExpression(initializer); - } - } - - return computeConstantExpression(expression); + return computeConstantExpression(returnExpr); } } } diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 0f19838ae..796a7ec31 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -86,11 +86,8 @@ - + - - - diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties index 03a323491..d1d5be302 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties @@ -9,8 +9,9 @@ xlang.annotation.tag.not-allow-child=Tag ''{0}'' not allow child xlang.annotation.reference.vfs-file-not-found=The referenced vfs file ''{0}'' doesn''t exist xlang.annotation.reference.xdef-ref-not-found=No xdef defined node named ''{0}'' exists xlang.annotation.reference.xdef-ref-not-found-in-path=No xdef defined node named ''{0}'' in ''{1}'' -xlang.annotation.reference.xdef-unique-attr-no-found=No attribute named ''{0}'' in current node +xlang.annotation.reference.xdef-unique-attr-not-found=No attribute named ''{0}'' in current node xlang.annotation.reference.xdef-key-attr-not-found=No child node which has attribute named ''{0}'' exists +xlang.annotation.reference.default-value-ref-attr-not-found=No attribute named ''{0}'' in current node xlang.annotation.reference.x-prototype-no-parent=Only child node can define ''x:prototype'' xlang.annotation.reference.x-prototype-tag-not-found=No sibling node named ''{0}'' exists xlang.annotation.reference.x-prototype-attr-not-found=No sibling node which has attribute ''{0}={1}'' exists diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties index ac1b7a223..636386ae2 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties @@ -9,7 +9,8 @@ xlang.annotation.tag.not-allow-child=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5B xlang.annotation.reference.vfs-file-not-found=\u5F15\u7528\u7684 vfs \u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 xlang.annotation.reference.xdef-ref-not-found=\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 xlang.annotation.reference.xdef-ref-not-found-in-path=\u5728 ''{1}'' \u4E2D\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 -xlang.annotation.reference.xdef-unique-attr-no-found=\u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 +xlang.annotation.reference.xdef-unique-attr-not-found=\u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 +xlang.annotation.reference.default-value-ref-attr-not-found=\u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 xlang.annotation.reference.xdef-key-attr-not-found=\u5728\u5B50\u8282\u70B9\u4E2D\u672A\u5B9A\u4E49\u5C5E\u6027 ''{0}'' xlang.annotation.reference.x-prototype-no-parent=\u53EA\u6709\u5728\u5B50\u8282\u70B9\u4E0A\u624D\u80FD\u5B9A\u4E49\u5C5E\u6027 ''x:prototype'' xlang.annotation.reference.x-prototype-tag-not-found=\u4E0D\u5B58\u5728\u6807\u7B7E\u540D\u4E3A ''{0}'' \u7684\u5144\u5F1F\u8282\u70B9 diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 3594d69b9..76972f111 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -53,7 +53,7 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur // Note: 提前将被引用的 vfs 资源添加到 Project 中 addAllNopXDefsToProject(); - addVfsResourcesToProject("/nop/core/xlib/meta-gen.xlib"); + addVfsResourcesToProject("/nop/core/xlib/meta-gen.xlib", "/dict/core/std-domain.dict.yaml"); addAllTestVfsResourcesToProject(); }); } @@ -131,11 +131,15 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur } protected PsiElement getElementAtCaret() { + assertCaretExists(); + return myFixture.getFile().findElementAt(myFixture.getCaretOffset()); } /** 找到光标位置的 {@link XLangReference} 或者其他类型的唯一引用 */ protected PsiReference findReferenceAtCaret() { + assertCaretExists(); + // 实际有多个引用时,将构造返回 PsiMultiReference, // 其会按 PsiMultiReference#COMPARATOR 对引用排序得到优先引用, // 再调用该优先引用的 #resolve() 得到 PsiElement @@ -166,8 +170,14 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur } protected PsiElement[] getGotoTargets() { + assertCaretExists(); + return GotoDeclarationAction.findAllTargetElements(getProject(), myFixture.getEditor(), myFixture.getCaretOffset()); } + + private void assertCaretExists() { + assertTrue("No '' found in current text", myFixture.getCaretOffset() > 0); + } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java index 37328a357..cc47d5c59 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java @@ -209,20 +209,31 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { public void testGetReferencesFromXmlAttributeType() { // 声明属性将 引用 属性的类型定义 + // TODO 暂时无法通过分析 class 字节码得到可注册的数据域 +// // - #getName 返回引用值 +// doTest(""" +// +// +// +// """, "/dict/test/doc/child-type.dict.yaml#leaf"); +// // - #getName 返回字面量值 +// doTest(""" +// +// +// +// """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + // - 引用字典中定义的数据域 doTest(""" - - - """, "io.nop.xui.initialize.VueNodeStdDomainHandler"); - doTest(""" - - + - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + """, "/dict/core/std-domain.dict.yaml#string"); // 字典/枚举的 options 引用 doTest(""" @@ -249,22 +260,29 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // "io.nop.xlang.xdef.XDefOverride#MERGE"); -// -// // TODO 缺省属性值中 @attr: 引用 -// doTest(""" -// -// -// -// """, ""); -// doTest(""" -// -// -// -// """, ""); + + // 缺省属性值中 @attr: 引用 + doTest(""" + + + + """, "import#name=var-name"); + doTest(""" + + + + """, "var#type=!string"); + doTest(""" + + + + """, null); } public void testGetReferencesFromXmlAttribute() { @@ -282,7 +300,8 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { "ame=\"!var-name\""), "xdef:define#xdef:name=!var-name"); - doTest(readVfsResource("/test/doc/example.xdef").replace("name=\"string\"", "name=\"string\""), + doTest(readVfsResource("/test/doc/example.xdef").replace("me=\"string\""), "meta:define#xdef:unknown-attr=def-type"); doTest(""" @@ -363,10 +382,10 @@ public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { xmlns:a="a" xmlns:xpl="xpl" > - GenExtends xpl:lib="/test/reference/a.xlib"/> + ByMdxQuery xpl:lib="/test/reference/a.xlib"/> - """, ""); + """, "/test/reference/a.xlib#DoFindByMdxQuery"); } /** 通过在 text 中插入 <caret> 代表光标位置 */ diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index cb5c59d88..c3c0f4b4d 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -14,10 +14,12 @@ @name This is child name @xdef:unknown-attr This a unknown attribute --> - + + + - + - + + + + + + + diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java new file mode 100644 index 000000000..37de502ad --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2017-2024 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ +package io.nop.idea.plugin.lang; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase; +import io.nop.idea.plugin.lang.script.XLangScriptFileType; +import org.junit.Test; + +/** + * @author flytreeleft + * @date 2025-06-28 + */ +public class TestXLangScriptCompletionContributor extends LightPlatformCodeInsightFixture4TestCase { + private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); + + @Test + public void testImportCompletion() { + myFixture.configureByText("sample." + ext, "import io.nop"); + myFixture.type("."); + + LookupElement[] items = myFixture.completeBasic(); + assertTrue(items.length > 0); + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java new file mode 100644 index 000000000..5072e2695 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -0,0 +1,38 @@ +package io.nop.idea.plugin.lang; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.impl.DebugUtil; +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase; +import io.nop.idea.plugin.lang.script.XLangScriptFileType; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +/** + * @author flytreeleft + * @date 2025-06-28 + */ +public class TestXLangScriptParser extends LightPlatformCodeInsightFixture4TestCase { + private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); + + @Test + public void testImportStatement() { + String code = "import io.nop.core.model.query.QueryBeanHelper;"; + + checkMatchJavaParseTree(code); + } + + protected void checkMatchJavaParseTree(String code) { + PsiFile testFile = myFixture.configureByText("sample." + ext, code); + String testTree = toParseTreeText(testFile); + + PsiFile javaFile = myFixture.configureByText("sample.java", code); + String javaTree = toParseTreeText(javaFile); + + assertEquals(javaTree, testTree); + } + + protected String toParseTreeText(@NotNull PsiElement file) { + return DebugUtil.psiToString(file, false, false); + } +} diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index 6c919be85..35454bcd8 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -19,6 +19,12 @@ const ormTemplate = inject('nopOrmTemplate'); const mapper = ormTemplate.getRowMapper(rowType,false); + // alskdfjlaksdf + /** asdfasdf */ + let a = 12.3 + 0x234d + 0b101 + 123L + 'ab'; a.b?.c; + let b = ``` + abcd efg + ```; const ret = thisObj.invoke('doDeleteByQuery', { query, authObjName, refNamesToCheck, prepareQuery: (qry, ctx) => { -- Gitee From 321275f242c9ab7d3dabaa41d0b128869526480d Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 29 Jun 2025 15:09:19 +0800 Subject: [PATCH 26/82] =?UTF-8?q?nop-idea-pugin:=20=E6=9A=82=E6=97=B6?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E5=AE=9E=E7=8E=B0=E5=AF=B9=20c:script=20?= =?UTF-8?q?=E4=B8=AD=E4=BB=A3=E7=A0=81=E7=9A=84=E8=87=AA=E5=8A=A8=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=E3=80=81=E5=BC=95=E7=94=A8=E8=B7=B3=E8=BD=AC=E7=AD=89?= =?UTF-8?q?=EF=BC=8C=E4=BB=85=E6=94=AF=E6=8C=81=E5=85=B3=E9=94=AE=E5=AD=97?= =?UTF-8?q?=E9=AB=98=E4=BA=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/script/XLangScriptASTFactory.java | 37 ++++++------------- .../XLangScriptCompletionContributor.java | 29 +++------------ .../lang/script/XLangScriptLanguage.java | 3 +- .../lang/script/XLangScriptParserAdaptor.java | 37 +------------------ .../script/XLangScriptParserDefinition.java | 9 +---- .../script/XLangScriptSyntaxHighlighter.java | 36 ++++++++++++------ .../XLangScriptCompletionProvider.java | 21 +++++++++++ .../psi/ImportAsDeclarationElement.java | 16 ++++++++ .../lang/script/psi/ImportSourceElement.java | 16 ++++++++ .../src/main/resources/META-INF/plugin.xml | 4 +- .../test/resources/_vfs/test/reference/a.xlib | 16 ++++++++ 11 files changed, 117 insertions(+), 107 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationElement.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceElement.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java index 67d47dbd9..cb7e8f8ad 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java @@ -1,13 +1,13 @@ package io.nop.idea.plugin.lang.script; -import com.intellij.psi.JavaTokenType; -import com.intellij.psi.impl.java.stubs.JavaStubElementTypes; -import com.intellij.psi.impl.source.PsiJavaCodeReferenceElementImpl; +import com.intellij.lang.ASTFactory; import com.intellij.psi.impl.source.tree.CompositeElement; -import com.intellij.psi.impl.source.tree.JavaASTFactory; +import com.intellij.psi.impl.source.tree.CompositePsiElement; import com.intellij.psi.impl.source.tree.LeafElement; +import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.tree.IElementType; -import io.nop.xlang.parse.antlr.XLangLexer; +import io.nop.idea.plugin.lang.script.psi.ImportAsDeclarationElement; +import io.nop.idea.plugin.lang.script.psi.ImportSourceElement; import io.nop.xlang.parse.antlr.XLangParser; import org.antlr.intellij.adaptor.lexer.RuleIElementType; import org.antlr.intellij.adaptor.lexer.TokenIElementType; @@ -18,8 +18,9 @@ import org.jetbrains.annotations.Nullable; * @author flytreeleft * @date 2025-06-28 */ -public class XLangScriptASTFactory extends JavaASTFactory { +public class XLangScriptASTFactory extends ASTFactory { + /** 为 AST 树的父节点创建 {@link CompositePsiElement} */ @Override public CompositeElement createComposite(@NotNull IElementType type) { if (!(type instanceof RuleIElementType rule)) { @@ -27,35 +28,21 @@ public class XLangScriptASTFactory extends JavaASTFactory { } return switch (rule.getRuleIndex()) { - case XLangParser.RULE_moduleDeclaration_import -> // - (CompositeElement) JavaStubElementTypes.IMPORT_LIST.createCompositeNode(); case XLangParser.RULE_importAsDeclaration -> // - (CompositeElement) JavaStubElementTypes.IMPORT_STATEMENT.createCompositeNode(); - case XLangParser.RULE_ast_importSource, XLangParser.RULE_qualifiedName, - XLangParser.RULE_qualifiedName_name_, XLangParser.RULE_identifier -> // - new PsiJavaCodeReferenceElementImpl(); + new ImportAsDeclarationElement(rule); + case XLangParser.RULE_ast_importSource -> // + new ImportSourceElement(rule); default -> null; }; } + /** 为 AST 树的叶子节点创建 {@link com.intellij.psi.PsiElement PsiElement} */ @Override public @Nullable LeafElement createLeaf(@NotNull IElementType type, @NotNull CharSequence text) { if (!(type instanceof TokenIElementType token)) { return null; } - IElementType javaTokenType = switch (token.getANTLRTokenType()) { - case XLangLexer.Identifier -> // - JavaTokenType.IDENTIFIER; - case XLangLexer.Import -> // - JavaTokenType.IMPORT_KEYWORD; - case XLangLexer.Dot -> // - JavaTokenType.DOT; - case XLangLexer.SemiColon -> // - JavaTokenType.SEMICOLON; - default -> null; - }; - - return super.createLeaf(javaTokenType, text); + return new LeafPsiElement(token, text); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptCompletionContributor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptCompletionContributor.java index ffa36e3e2..8db650bd2 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptCompletionContributor.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptCompletionContributor.java @@ -1,37 +1,18 @@ package io.nop.idea.plugin.lang.script; import com.intellij.codeInsight.completion.CompletionContributor; -import com.intellij.codeInsight.completion.CompletionInitializationContext; -import com.intellij.codeInsight.completion.CompletionParameters; -import com.intellij.codeInsight.completion.CompletionResultSet; -import com.intellij.codeInsight.completion.JavaCompletionContributor; -import com.intellij.openapi.editor.Editor; +import com.intellij.codeInsight.completion.CompletionType; import com.intellij.openapi.project.DumbAware; -import com.intellij.openapi.util.NlsContexts; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import com.intellij.patterns.PlatformPatterns; +import io.nop.idea.plugin.lang.script.completion.XLangScriptCompletionProvider; /** * @author flytreeleft * @date 2025-06-28 */ public class XLangScriptCompletionContributor extends CompletionContributor implements DumbAware { - private static final JavaCompletionContributor java = new JavaCompletionContributor(); - @Override - public void beforeCompletion(@NotNull CompletionInitializationContext context) { - java.beforeCompletion(context); - } - - @Override - public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) { - java.fillCompletionVariants(parameters, result); - } - - @Override - public @Nullable @NlsContexts.HintText String handleEmptyLookup( - @NotNull CompletionParameters parameters, Editor editor - ) { - return java.handleEmptyLookup(parameters, editor); + public XLangScriptCompletionContributor() { + extend(CompletionType.BASIC, PlatformPatterns.psiElement(), new XLangScriptCompletionProvider()); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptLanguage.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptLanguage.java index de6e48b88..0ea0efd7e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptLanguage.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptLanguage.java @@ -1,7 +1,6 @@ package io.nop.idea.plugin.lang.script; import com.intellij.lang.Language; -import com.intellij.lang.java.JavaLanguage; /** * @author flytreeleft @@ -11,6 +10,6 @@ public class XLangScriptLanguage extends Language { public static final XLangScriptLanguage INSTANCE = new XLangScriptLanguage(); private XLangScriptLanguage() { - super(JavaLanguage.INSTANCE, "XLangScript"); + super((Language) null, "XLangScript"); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java index 154d1f246..51af0b8b4 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java @@ -1,16 +1,11 @@ package io.nop.idea.plugin.lang.script; -import java.util.ArrayList; -import java.util.List; - import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.IFileElementType; import io.nop.xlang.parse.antlr.XLangParser; import org.antlr.intellij.adaptor.parser.ANTLRParserAdaptor; import org.antlr.v4.runtime.Parser; -import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.TerminalNode; /** * @author flytreeleft @@ -24,37 +19,9 @@ public class XLangScriptParserAdaptor extends ANTLRParserAdaptor { @Override protected ParseTree parse(Parser parser, IElementType root) { - ParseTree tree; - if (root instanceof IFileElementType) { - tree = ((XLangParser) parser).program(); - } else { - tree = ((XLangParser) parser).statement(); - } - return convertParseTree(tree); - } - - protected ParseTree convertParseTree(ParseTree tree) { - if (tree instanceof TerminalNode) { - return tree; - } // - else if (tree instanceof XLangParser.Eos__Context eos) { - TerminalNode child = eos.SemiColon(); - - return child != null ? child : eos.EOF(); - } // - else if (tree instanceof ParserRuleContext ctx) { - List children = new ArrayList<>(ctx.children.size()); - ctx.children.forEach((child) -> { - child = convertParseTree(child); - if (child != null) { - children.add(child); - } - }); - - ctx.children = children; + return ((XLangParser) parser).program(); } - - return tree; + return ((XLangParser) parser).statement(); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index f415f6ed8..f59eb500c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -10,8 +10,6 @@ import com.intellij.openapi.project.Project; import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; -import com.intellij.psi.impl.java.stubs.JavaStubElementType; -import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.IFileElementType; import com.intellij.psi.tree.TokenSet; import io.nop.xlang.parse.antlr.XLangLexer; @@ -69,12 +67,7 @@ public class XLangScriptParserDefinition implements ParserDefinition { @NotNull @Override public PsiElement createElement(ASTNode node) { - IElementType type = node.getElementType(); - if (type instanceof JavaStubElementType) { - return ((JavaStubElementType) type).createPsi(node); - } - - //throw new IllegalArgumentException("Not a Java node: " + node + " (" + type + ", " + type.getLanguage() + ")"); + // Note: 只有在 ASTFactory 中未创建 PsiElement 的节点才会调用该接口 return new ANTLRPsiNode(node); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptSyntaxHighlighter.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptSyntaxHighlighter.java index 141895cb6..c7f950841 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptSyntaxHighlighter.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptSyntaxHighlighter.java @@ -41,19 +41,10 @@ public class XLangScriptSyntaxHighlighter extends SyntaxHighlighterBase { TextAttributesKey attrKey = switch (myType.getANTLRTokenType()) { case XLangLexer.Identifier -> // DefaultLanguageHighlighterColors.IDENTIFIER; - case XLangLexer.Const, XLangLexer.Let, XLangLexer.While, // - XLangLexer.If, XLangLexer.Else, XLangLexer.Return, // - XLangLexer.Import, XLangLexer.Function, XLangLexer.New, // - XLangLexer.Boolean, XLangLexer.NullLiteral, XLangLexer.BooleanLiteral, // - XLangLexer.AndLiteral, XLangLexer.OrLiteral // - -> // - JavaHighlightingColors.KEYWORD; - case XLangLexer.StringLiteral, XLangLexer.TemplateStringLiteral // - -> // + case XLangLexer.StringLiteral, XLangLexer.TemplateStringLiteral -> // JavaHighlightingColors.STRING; case XLangLexer.DecimalIntegerLiteral, XLangLexer.HexIntegerLiteral, // - XLangLexer.BinaryIntegerLiteral, XLangLexer.DecimalLiteral // - -> // + XLangLexer.BinaryIntegerLiteral, XLangLexer.DecimalLiteral -> // JavaHighlightingColors.NUMBER; case XLangLexer.OpenBracket, XLangLexer.CloseBracket, XLangLexer.CpExprStart -> // JavaHighlightingColors.BRACKETS; @@ -73,6 +64,29 @@ public class XLangScriptSyntaxHighlighter extends SyntaxHighlighterBase { JavaHighlightingColors.LINE_COMMENT; case XLangLexer.MultiLineComment -> // JavaHighlightingColors.JAVA_BLOCK_COMMENT; + case XLangLexer.Break, XLangLexer.Do, XLangLexer.Instanceof, // + XLangLexer.Typeof, XLangLexer.Case, XLangLexer.Else, // + XLangLexer.New, XLangLexer.Var, XLangLexer.Catch, // + XLangLexer.Finally, XLangLexer.Return, XLangLexer.Void, // + XLangLexer.Continue, XLangLexer.For, XLangLexer.Switch, // + XLangLexer.While, XLangLexer.Debugger, XLangLexer.Function, // + XLangLexer.This, XLangLexer.With, XLangLexer.Default, // + XLangLexer.If, XLangLexer.Throw, XLangLexer.Delete, // + XLangLexer.In, XLangLexer.Try, XLangLexer.As, // + XLangLexer.From, XLangLexer.ReadOnly, XLangLexer.Async, // + XLangLexer.Await, XLangLexer.Class, XLangLexer.Enum, // + XLangLexer.Extends, XLangLexer.Super, XLangLexer.Const, // + XLangLexer.Export, XLangLexer.Import, // + XLangLexer.Implements, XLangLexer.Let, XLangLexer.Private, // + XLangLexer.Public, XLangLexer.Interface, XLangLexer.Package,// + XLangLexer.Protected, XLangLexer.Static, // + XLangLexer.Any, XLangLexer.Number, XLangLexer.Boolean, // + XLangLexer.String, XLangLexer.Symbol, XLangLexer.TypeAlias, // + XLangLexer.Constructor, XLangLexer.Abstract, // + // + XLangLexer.NullLiteral, XLangLexer.BooleanLiteral, // + XLangLexer.AndLiteral, XLangLexer.OrLiteral -> // + JavaHighlightingColors.KEYWORD; default -> null; }; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java new file mode 100644 index 000000000..cb8520b8a --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java @@ -0,0 +1,21 @@ +package io.nop.idea.plugin.lang.script.completion; + +import com.intellij.codeInsight.completion.CompletionParameters; +import com.intellij.codeInsight.completion.CompletionProvider; +import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.util.ProcessingContext; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-29 + */ +public class XLangScriptCompletionProvider extends CompletionProvider { + + @Override + protected void addCompletions( + @NotNull CompletionParameters parameters, @NotNull ProcessingContext context, + @NotNull CompletionResultSet result + ) { + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationElement.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationElement.java new file mode 100644 index 000000000..babb276ca --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationElement.java @@ -0,0 +1,16 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.psi.impl.source.tree.CompositePsiElement; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-29 + */ +public class ImportAsDeclarationElement extends CompositePsiElement { + + public ImportAsDeclarationElement(@NotNull IElementType type) { + super(type); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceElement.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceElement.java new file mode 100644 index 000000000..fc22aecf5 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceElement.java @@ -0,0 +1,16 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.psi.impl.source.tree.CompositePsiElement; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-29 + */ +public class ImportSourceElement extends CompositePsiElement { + + public ImportSourceElement(@NotNull IElementType type) { + super(type); + } +} diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 1c36e8baa..75ea3669f 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -121,9 +121,9 @@ implementationClass="io.nop.idea.plugin.lang.script.XLangScriptParserDefinition"/> - + implementationClass="io.nop.idea.plugin.lang.script.XLangScriptCompletionContributor"/>--> diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index 35454bcd8..b42753f67 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -19,6 +19,22 @@ const ormTemplate = inject('nopOrmTemplate'); const mapper = ormTemplate.getRowMapper(rowType,false); + while (true) { + let a = 's' instanceof string; + let b = #{'ab'}; + break; + } + try { + for (let i = 0; i < 10; i++) { + const a = new A(); + if (typeof a == 'boolean') { + continue; + } + } + } catch(e) { + throw e; + } finally {} + // alskdfjlaksdf /** asdfasdf */ let a = 12.3 + 0x234d + 0b101 + 123L + 'ab'; a.b?.c; -- Gitee From 6819a78df39fa83e440a2293f6b92f5ab921b355 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 29 Jun 2025 22:17:38 +0800 Subject: [PATCH 27/82] =?UTF-8?q?nop-idea-pugin:=20=E5=88=9D=E6=AD=A5?= =?UTF-8?q?=E8=B7=91=E9=80=9A=E5=AF=B9=20XLang=20Script=20=E7=9A=84?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=A1=A5=E5=85=A8=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/script/XLangScriptASTFactory.java | 23 +----- .../script/XLangScriptParserDefinition.java | 23 +++++- .../XLangScriptCompletionProvider.java | 77 +++++++++++++++++++ ...ment.java => ImportAsDeclarationNode.java} | 9 +-- ...ment.java => ImportQualifiedNameNode.java} | 9 +-- .../lang/script/psi/ImportSourceNode.java | 15 ++++ .../plugin/lang/script/psi/ProgramNode.java | 22 ++++++ .../plugin/lang/script/psi/RuleSpecNode.java | 21 +++++ .../src/main/resources/META-INF/plugin.xml | 36 ++++----- .../idea/plugin/BaseXLangPluginTestCase.java | 25 ++++++ .../TestXLangScriptCompletionContributor.java | 30 +++++--- .../test/resources/_vfs/test/reference/a.xlib | 1 + 12 files changed, 228 insertions(+), 63 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/{ImportSourceElement.java => ImportAsDeclarationNode.java} (43%) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/{ImportAsDeclarationElement.java => ImportQualifiedNameNode.java} (42%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java index cb7e8f8ad..cc6deb8cc 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java @@ -1,15 +1,9 @@ package io.nop.idea.plugin.lang.script; import com.intellij.lang.ASTFactory; -import com.intellij.psi.impl.source.tree.CompositeElement; -import com.intellij.psi.impl.source.tree.CompositePsiElement; import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.tree.IElementType; -import io.nop.idea.plugin.lang.script.psi.ImportAsDeclarationElement; -import io.nop.idea.plugin.lang.script.psi.ImportSourceElement; -import io.nop.xlang.parse.antlr.XLangParser; -import org.antlr.intellij.adaptor.lexer.RuleIElementType; import org.antlr.intellij.adaptor.lexer.TokenIElementType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,21 +14,8 @@ import org.jetbrains.annotations.Nullable; */ public class XLangScriptASTFactory extends ASTFactory { - /** 为 AST 树的父节点创建 {@link CompositePsiElement} */ - @Override - public CompositeElement createComposite(@NotNull IElementType type) { - if (!(type instanceof RuleIElementType rule)) { - return null; - } - - return switch (rule.getRuleIndex()) { - case XLangParser.RULE_importAsDeclaration -> // - new ImportAsDeclarationElement(rule); - case XLangParser.RULE_ast_importSource -> // - new ImportSourceElement(rule); - default -> null; - }; - } + // Note: 封装 CompositeElement,直接在 XLangScriptParserDefinition#createElement + // 中创建 PsiElement,避免非必要对象的定义和创建 /** 为 AST 树的叶子节点创建 {@link com.intellij.psi.PsiElement PsiElement} */ @Override diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index f59eb500c..4f4dfdab4 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -12,11 +12,16 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.tree.IFileElementType; import com.intellij.psi.tree.TokenSet; +import io.nop.idea.plugin.lang.script.psi.ImportAsDeclarationNode; +import io.nop.idea.plugin.lang.script.psi.ImportQualifiedNameNode; +import io.nop.idea.plugin.lang.script.psi.ImportSourceNode; +import io.nop.idea.plugin.lang.script.psi.ProgramNode; +import io.nop.idea.plugin.lang.script.psi.RuleSpecNode; import io.nop.xlang.parse.antlr.XLangLexer; import io.nop.xlang.parse.antlr.XLangParser; import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; +import org.antlr.intellij.adaptor.lexer.RuleIElementType; import org.antlr.intellij.adaptor.lexer.TokenIElementType; -import org.antlr.intellij.adaptor.psi.ANTLRPsiNode; import org.jetbrains.annotations.NotNull; /** @@ -67,8 +72,22 @@ public class XLangScriptParserDefinition implements ParserDefinition { @NotNull @Override public PsiElement createElement(ASTNode node) { + if (!(node.getElementType() instanceof RuleIElementType rule)) { + return new RuleSpecNode(node); + } + // Note: 只有在 ASTFactory 中未创建 PsiElement 的节点才会调用该接口 - return new ANTLRPsiNode(node); + return switch (rule.getRuleIndex()) { + case XLangParser.RULE_importAsDeclaration -> // + new ImportAsDeclarationNode(node); + case XLangParser.RULE_ast_importSource -> // + new ImportSourceNode(node); + case XLangParser.RULE_qualifiedName -> // + new ImportQualifiedNameNode(node); + case XLangParser.RULE_program -> // + new ProgramNode(node); + default -> new RuleSpecNode(node); + }; } @NotNull diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java index cb8520b8a..73748b808 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java @@ -3,7 +3,17 @@ package io.nop.idea.plugin.lang.script.completion; import com.intellij.codeInsight.completion.CompletionParameters; import com.intellij.codeInsight.completion.CompletionProvider; import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.codeInsight.completion.InsertionContext; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.searches.AllClassesSearch; import com.intellij.util.ProcessingContext; +import io.nop.idea.plugin.lang.script.psi.ImportSourceNode; import org.jetbrains.annotations.NotNull; /** @@ -17,5 +27,72 @@ public class XLangScriptCompletionProvider extends CompletionProvider { + String fqn = cls.getQualifiedName(); + if (fqn == null || fqn.equals(pkgName) || (!pkgName.isEmpty() && !fqn.startsWith(pkgName))) { + return; + } + + result.addElement(LookupElementBuilder.create(fqn) + .withIcon(AllIcons.Nodes.Class) + .withInsertHandler((ctx, item) -> { + addImportStatement(ctx, pkgName); + })); + }); + } + + private void addImportStatement(InsertionContext context, String pkgName) { + // 补全插入的后处理 + // TODO 对 Document 的修改可能会导致 AST 重建,从而使得对字符的定位不准,需调整补全插入思路 + Editor editor = context.getEditor(); + Document document = editor.getDocument(); + + // 补全插入的开始位置 + int startOffset = context.getStartOffset(); + // 补全插入的结束位置(默认光标位置) + int tailOffset = context.getTailOffset(); + // 触发补全的字符(Enter, Tab 等) + char completionChar = context.getCompletionChar(); + + int endOffset = startOffset + pkgName.length() - 2; + document.deleteString(startOffset, endOffset); + +// // 添加括号 +// document.insertString(tailOffset, "()"); +// // 移动光标到括号内 +// editor.getCaretModel().moveToOffset(tailOffset + 1); + +// // 插入带占位符的参数列表 +// document.insertString(tailOffset, "(, )"); +// // 选择第一个参数占位符 +// int start = tailOffset + 1; +// int end = start + "".length(); +// editor.getSelectionModel().setSelection(start, end); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceElement.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationNode.java similarity index 43% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceElement.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationNode.java index fc22aecf5..c0204965c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceElement.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationNode.java @@ -1,16 +1,15 @@ package io.nop.idea.plugin.lang.script.psi; -import com.intellij.psi.impl.source.tree.CompositePsiElement; -import com.intellij.psi.tree.IElementType; +import com.intellij.lang.ASTNode; import org.jetbrains.annotations.NotNull; /** * @author flytreeleft * @date 2025-06-29 */ -public class ImportSourceElement extends CompositePsiElement { +public class ImportAsDeclarationNode extends RuleSpecNode { - public ImportSourceElement(@NotNull IElementType type) { - super(type); + public ImportAsDeclarationNode(@NotNull ASTNode node) { + super(node); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationElement.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportQualifiedNameNode.java similarity index 42% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationElement.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportQualifiedNameNode.java index babb276ca..28cf57285 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationElement.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportQualifiedNameNode.java @@ -1,16 +1,15 @@ package io.nop.idea.plugin.lang.script.psi; -import com.intellij.psi.impl.source.tree.CompositePsiElement; -import com.intellij.psi.tree.IElementType; +import com.intellij.lang.ASTNode; import org.jetbrains.annotations.NotNull; /** * @author flytreeleft * @date 2025-06-29 */ -public class ImportAsDeclarationElement extends CompositePsiElement { +public class ImportQualifiedNameNode extends RuleSpecNode { - public ImportAsDeclarationElement(@NotNull IElementType type) { - super(type); + public ImportQualifiedNameNode(@NotNull ASTNode node) { + super(node); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java new file mode 100644 index 000000000..3b41cfed2 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -0,0 +1,15 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-29 + */ +public class ImportSourceNode extends RuleSpecNode { + + public ImportSourceNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java new file mode 100644 index 000000000..257501e83 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java @@ -0,0 +1,22 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 注意,当 XScript 是嵌入在 <c:script/> 标签中时, + * {@link #getContext()} 或 {@link #getParent()} + * 的结果为该标签节点,通过该节点向上可得到在 xlib 函数中定义的参数, + * 从而可将其用于代码补全、引用跳转等 + * + * @author flytreeleft + * @date 2025-06-29 + */ +public class ProgramNode extends RuleSpecNode { + + public ProgramNode(@NotNull ASTNode node) { + super(node); + } + + // TODO 获取上下文环境中可访问的变量 +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java new file mode 100644 index 000000000..9202d7cd6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java @@ -0,0 +1,21 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.extapi.psi.ASTWrapperPsiElement; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import org.antlr.intellij.adaptor.psi.Trees; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-29 + */ +public class RuleSpecNode extends ASTWrapperPsiElement { + + public RuleSpecNode(@NotNull ASTNode node) { + super(node); + } + + @Override + public PsiElement @NotNull [] getChildren() {return Trees.getChildren(this);} +} diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 75ea3669f..a1484af37 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -74,44 +74,38 @@ + + + + + + + - - - - - - - - - - - - - - - + + - + implementationClass="io.nop.idea.plugin.lang.script.XLangScriptCompletionContributor"/> diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 76972f111..1b6166978 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -5,11 +5,16 @@ import java.io.FileInputStream; import java.nio.charset.Charset; import java.nio.file.FileVisitResult; import java.util.ArrayList; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import com.intellij.codeInsight.TargetElementUtil; import com.intellij.codeInsight.documentation.DocumentationManager; +import com.intellij.codeInsight.lookup.Lookup; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupManager; +import com.intellij.codeInsight.lookup.impl.LookupImpl; import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.openapi.application.ApplicationManager; @@ -64,6 +69,26 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur super.tearDown(); } + /** 自动补齐测试:默认选中第一个补全项 */ + protected void doTestCompletion(String expectedText) { + // 获取当前查找元素 + LookupImpl lookup = (LookupImpl) LookupManager.getActiveLookup(myFixture.getEditor()); + assertNotNull("Lookup not active", lookup); + + List items = lookup.getItems(); + assertFalse("No completion items", items.isEmpty()); + + // 选择第一个补全项 + LookupElement item = items.get(0); + lookup.setCurrentItem(item); + + // 模拟选中补全项 + lookup.finishLookup(Lookup.NORMAL_SELECT_CHAR); + + // 验证结果 + myFixture.checkResult(expectedText); + } + protected void configureByXLangText(String text) { myFixture.configureByText("unit." + XLANG_EXT, text); } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java index 37de502ad..91a04b881 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java @@ -7,24 +7,36 @@ */ package io.nop.idea.plugin.lang; -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase; +import java.util.List; + +import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.idea.plugin.lang.script.XLangScriptFileType; -import org.junit.Test; /** * @author flytreeleft * @date 2025-06-28 */ -public class TestXLangScriptCompletionContributor extends LightPlatformCodeInsightFixture4TestCase { +public class TestXLangScriptCompletionContributor extends BaseXLangPluginTestCase { private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); - @Test public void testImportCompletion() { - myFixture.configureByText("sample." + ext, "import io.nop"); - myFixture.type("."); + String[] samples = new String[] { + //"io.nop.xlang.", // + "io.nop.xlang.x", // + "io.nop.xu", // + }; + + for (String sample : samples) { + myFixture.configureByText("sample." + ext, "import " + sample + ";"); + myFixture.completeBasic(); + + List items = myFixture.getLookupElementStrings(); + assertNotNull(items); + assertFalse(items.isEmpty()); + + items.forEach((item) -> assertTrue(item.startsWith(sample))); - LookupElement[] items = myFixture.completeBasic(); - assertTrue(items.length > 0); + doTestCompletion("import " + items.get(0) + ";"); + } } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index b42753f67..56cad9f21 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -13,6 +13,7 @@ Date: Mon, 30 Jun 2025 13:00:44 +0800 Subject: [PATCH 28/82] =?UTF-8?q?nop-idea-pugin:=20=E5=BA=9F=E5=BC=83?= =?UTF-8?q?=E5=AF=B9=20XLang=20Script=20=E7=9A=84=20CompletionProvider=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=EF=BC=8C=E6=94=B9=E4=B8=BA=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E8=A1=A5=E5=85=85=20PsiElement#getReferences=20=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=20Java=20=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E5=AF=B9=E8=B1=A1=E7=9A=84=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E8=87=AA=E5=8A=A8=E5=AE=9E=E7=8E=B0=E5=AF=B9=20import?= =?UTF-8?q?=20=E5=8C=85=E5=92=8C=E7=B1=BB=E7=9A=84=E8=A1=A5=E5=85=A8?= =?UTF-8?q?=E3=80=81=E5=BC=95=E7=94=A8=E8=B7=B3=E8=BD=AC=E3=80=81=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=98=BE=E7=A4=BA=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/script/XLangScriptASTFactory.java | 7 ++- .../XLangScriptCompletionContributor.java | 3 ++ .../XLangScriptCompletionProvider.java | 2 + .../lang/script/psi/IdentifierNode.java | 16 +++++++ .../lang/script/psi/ImportSourceNode.java | 17 ++++++++ .../src/main/resources/META-INF/plugin.xml | 7 ++- .../idea/plugin/BaseXLangPluginTestCase.java | 40 ++++++++--------- ...r.java => TestXLangScriptCompletions.java} | 17 +++++--- .../lang/TestXLangScriptReferences.java | 43 +++++++++++++++++++ .../test/resources/_vfs/test/reference/a.xlib | 2 +- 10 files changed, 123 insertions(+), 31 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java rename nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/{TestXLangScriptCompletionContributor.java => TestXLangScriptCompletions.java} (63%) create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java index cc6deb8cc..da8c51f00 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java @@ -4,6 +4,8 @@ import com.intellij.lang.ASTFactory; import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.tree.IElementType; +import io.nop.idea.plugin.lang.script.psi.IdentifierNode; +import io.nop.xlang.parse.antlr.XLangLexer; import org.antlr.intellij.adaptor.lexer.TokenIElementType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,6 +26,9 @@ public class XLangScriptASTFactory extends ASTFactory { return null; } - return new LeafPsiElement(token, text); + return switch (token.getANTLRTokenType()) { + case XLangLexer.Identifier -> new IdentifierNode(token, text); + default -> new LeafPsiElement(token, text); + }; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptCompletionContributor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptCompletionContributor.java index 8db650bd2..547e10583 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptCompletionContributor.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptCompletionContributor.java @@ -4,12 +4,15 @@ import com.intellij.codeInsight.completion.CompletionContributor; import com.intellij.codeInsight.completion.CompletionType; import com.intellij.openapi.project.DumbAware; import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.PsiElement; import io.nop.idea.plugin.lang.script.completion.XLangScriptCompletionProvider; /** * @author flytreeleft * @date 2025-06-28 + * @deprecated 通过实现 {@link PsiElement#getReferences()} 并返回 Java 相关的引用对象,便可实现自动补全,无需单独编写逻辑 */ +@Deprecated public class XLangScriptCompletionContributor extends CompletionContributor implements DumbAware { public XLangScriptCompletionContributor() { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java index 73748b808..0a14f5f11 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java @@ -19,7 +19,9 @@ import org.jetbrains.annotations.NotNull; /** * @author flytreeleft * @date 2025-06-29 + * @deprecated 通过实现 {@link PsiElement#getReferences()} 并返回 Java 相关的引用对象,便可实现自动补全,无需单独编写逻辑 */ +@Deprecated public class XLangScriptCompletionProvider extends CompletionProvider { @Override diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java new file mode 100644 index 000000000..d418a58c3 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java @@ -0,0 +1,16 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-30 + */ +public class IdentifierNode extends LeafPsiElement { + + public IdentifierNode(@NotNull IElementType type, @NotNull CharSequence text) { + super(type, text); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java index 3b41cfed2..3b439ee68 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -1,6 +1,9 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceProvider; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceSet; import org.jetbrains.annotations.NotNull; /** @@ -12,4 +15,18 @@ public class ImportSourceNode extends RuleSpecNode { public ImportSourceNode(@NotNull ASTNode node) { super(node); } + + /** 构造 Java 相关的引用对象,从而支持自动补全、引用跳转、文档显示等 */ + @Override + public PsiReference @NotNull [] getReferences() { + String fqn = getText(); + + JavaClassReferenceProvider provider = new JavaClassReferenceProvider(); + // 支持解析包名:JavaClassReference#advancedResolveInner + provider.setOption(JavaClassReferenceProvider.ADVANCED_RESOLVE, true); + + JavaClassReferenceSet refSet = new JavaClassReferenceSet(fqn, this, 0, false, provider); + + return refSet.getReferences(); + } } diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index a1484af37..2eca83215 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -117,7 +117,10 @@ implementationClass="io.nop.idea.plugin.lang.script.XLangScriptSyntaxHighlighter"/> - + + diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 1b6166978..323b82603 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -69,26 +69,6 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur super.tearDown(); } - /** 自动补齐测试:默认选中第一个补全项 */ - protected void doTestCompletion(String expectedText) { - // 获取当前查找元素 - LookupImpl lookup = (LookupImpl) LookupManager.getActiveLookup(myFixture.getEditor()); - assertNotNull("Lookup not active", lookup); - - List items = lookup.getItems(); - assertFalse("No completion items", items.isEmpty()); - - // 选择第一个补全项 - LookupElement item = items.get(0); - lookup.setCurrentItem(item); - - // 模拟选中补全项 - lookup.finishLookup(Lookup.NORMAL_SELECT_CHAR); - - // 验证结果 - myFixture.checkResult(expectedText); - } - protected void configureByXLangText(String text) { myFixture.configureByText("unit." + XLANG_EXT, text); } @@ -205,4 +185,24 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur private void assertCaretExists() { assertTrue("No '' found in current text", myFixture.getCaretOffset() > 0); } + + /** 检查自动补全所选中的第一个补全项是否与预期相符 */ + protected void assertCompletion(String expectedText) { + // 获取当前查找元素 + LookupImpl lookup = (LookupImpl) LookupManager.getActiveLookup(myFixture.getEditor()); + assertNotNull("Lookup not active", lookup); + + List items = lookup.getItems(); + assertFalse("No completion items", items.isEmpty()); + + // 选择第一个补全项 + LookupElement item = items.get(0); + lookup.setCurrentItem(item); + + // 模拟选中补全项 + lookup.finishLookup(Lookup.NORMAL_SELECT_CHAR); + + // 验证结果 + myFixture.checkResult(expectedText); + } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java similarity index 63% rename from nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java rename to nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java index 91a04b881..52868d1e7 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletionContributor.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java @@ -16,27 +16,30 @@ import io.nop.idea.plugin.lang.script.XLangScriptFileType; * @author flytreeleft * @date 2025-06-28 */ -public class TestXLangScriptCompletionContributor extends BaseXLangPluginTestCase { +public class TestXLangScriptCompletions extends BaseXLangPluginTestCase { private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); public void testImportCompletion() { + // Note: 不能构造仅匹配唯一结果的样本,以避免 myFixture.getLookupElementStrings() 返回 null 或空结果 String[] samples = new String[] { - //"io.nop.xlang.", // - "io.nop.xlang.x", // - "io.nop.xu", // + // 对包的补全 + "io.nop.xlang.", // + "io.nop.x", // + // 对类的补全 + "io.nop.xlang.xdef.XD", // }; for (String sample : samples) { myFixture.configureByText("sample." + ext, "import " + sample + ";"); myFixture.completeBasic(); + // Note: 在仅有唯一匹配时,得到的结果为 null 或空 List items = myFixture.getLookupElementStrings(); assertNotNull(items); assertFalse(items.isEmpty()); - items.forEach((item) -> assertTrue(item.startsWith(sample))); - - doTestCompletion("import " + items.get(0) + ";"); + String expected = "import " + sample.replaceAll("\\.[^.]+$", ".") + items.get(0) + ";"; + assertCompletion(expected); } } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java new file mode 100644 index 000000000..9a73a6dae --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2017-2024 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ +package io.nop.idea.plugin.lang; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import io.nop.idea.plugin.BaseXLangPluginTestCase; +import io.nop.idea.plugin.lang.script.XLangScriptFileType; + +/** + * @author flytreeleft + * @date 2025-06-30 + */ +public class TestXLangScriptReferences extends BaseXLangPluginTestCase { + private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); + + public void testImportReference() { + // Note: 需确保语法完整 + + // 导入包:只能得到光标之前的已存在包 + doTest("import io.nop.xlang.xdef;", "io.nop.xlang"); + + // 导入类 + doTest("import io.nop.xlang.xdef.XDefOverride;", "io.nop.xlang.xdef"); + doTest("import io.nop.xlang.xdef.XDefOverride;", "io.nop.xlang.xdef.XDefOverride"); + } + + /** 通过在 text 中插入 <caret> 代表光标位置 */ + private void doTest(String text, String expected) { + myFixture.configureByText("sample." + ext, text); + + PsiReference ref = findReferenceAtCaret(); + assertNotNull(ref); + + PsiElement target = ref.resolve(); + assertNotNull(target); + } +} diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index 56cad9f21..107202d42 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -13,7 +13,7 @@ Date: Mon, 30 Jun 2025 18:05:51 +0800 Subject: [PATCH 29/82] =?UTF-8?q?nop-idea-pugin:=20=E4=B8=BA=20XLang=20Scr?= =?UTF-8?q?ipt=20=E4=B8=AD=E7=9A=84=E4=B8=BB=E8=A6=81=20AST=20=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=AE=9A=E4=B9=89=20PsiElement=20=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../script/XLangScriptParserDefinition.java | 51 +++++- ...onNode.java => ArrowFunctionBodyNode.java} | 6 +- .../lang/script/psi/ArrowFunctionNode.java | 17 ++ .../lang/script/psi/BlockStatementNode.java | 19 +++ .../lang/script/psi/CalleeArgumentsNode.java | 17 ++ .../script/psi/ExpressionElementNode.java | 29 ++++ ...Node.java => FunctionDeclarationNode.java} | 6 +- .../psi/FunctionParameterDeclarationNode.java | 17 ++ .../script/psi/ImportDeclarationNode.java | 17 ++ .../lang/script/psi/ImportSourceNode.java | 2 + .../psi/ObjectPropertyAssignmentNode.java | 19 +++ .../lang/script/psi/ObjectPropertyNode.java | 20 +++ .../script/psi/ParameterizedTypeNode.java | 17 ++ .../lang/script/psi/StatementRootNode.java | 19 +++ .../script/psi/VariableDeclarationNode.java | 17 ++ .../plugin/lang/TestXLangScriptParser.java | 54 +++++-- .../resources/_vfs/test/ast/statement-1.ast | 24 +++ .../resources/_vfs/test/ast/statement-2.ast | 45 ++++++ .../resources/_vfs/test/ast/statement-3.ast | 92 +++++++++++ .../resources/_vfs/test/ast/statement-4.ast | 142 +++++++++++++++++ .../resources/_vfs/test/ast/statement-5.ast | 145 ++++++++++++++++++ .../_vfs/test/ast/statement-err-1.ast | 25 +++ .../_vfs/test/ast/statement-err-2.ast | 21 +++ .../_vfs/test/ast/statement-err-3.ast | 26 ++++ .../test/resources/_vfs/test/reference/a.xlib | 1 - 25 files changed, 822 insertions(+), 26 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/{ImportAsDeclarationNode.java => ArrowFunctionBodyNode.java} (62%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionElementNode.java rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/{ImportQualifiedNameNode.java => FunctionDeclarationNode.java} (62%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementRootNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-2.ast create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-3.ast create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-4.ast create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-2.ast create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-3.ast diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index 4f4dfdab4..f289ae117 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -12,11 +12,22 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.tree.IFileElementType; import com.intellij.psi.tree.TokenSet; -import io.nop.idea.plugin.lang.script.psi.ImportAsDeclarationNode; -import io.nop.idea.plugin.lang.script.psi.ImportQualifiedNameNode; +import io.nop.idea.plugin.lang.script.psi.ArrowFunctionBodyNode; +import io.nop.idea.plugin.lang.script.psi.ArrowFunctionNode; +import io.nop.idea.plugin.lang.script.psi.BlockStatementNode; +import io.nop.idea.plugin.lang.script.psi.CalleeArgumentsNode; +import io.nop.idea.plugin.lang.script.psi.ExpressionElementNode; +import io.nop.idea.plugin.lang.script.psi.FunctionDeclarationNode; +import io.nop.idea.plugin.lang.script.psi.FunctionParameterDeclarationNode; +import io.nop.idea.plugin.lang.script.psi.ImportDeclarationNode; import io.nop.idea.plugin.lang.script.psi.ImportSourceNode; +import io.nop.idea.plugin.lang.script.psi.ObjectPropertyAssignmentNode; +import io.nop.idea.plugin.lang.script.psi.ObjectPropertyNode; +import io.nop.idea.plugin.lang.script.psi.ParameterizedTypeNode; import io.nop.idea.plugin.lang.script.psi.ProgramNode; import io.nop.idea.plugin.lang.script.psi.RuleSpecNode; +import io.nop.idea.plugin.lang.script.psi.StatementRootNode; +import io.nop.idea.plugin.lang.script.psi.VariableDeclarationNode; import io.nop.xlang.parse.antlr.XLangLexer; import io.nop.xlang.parse.antlr.XLangParser; import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; @@ -78,14 +89,40 @@ public class XLangScriptParserDefinition implements ParserDefinition { // Note: 只有在 ASTFactory 中未创建 PsiElement 的节点才会调用该接口 return switch (rule.getRuleIndex()) { + case XLangParser.RULE_program -> // + new ProgramNode(node); + case XLangParser.RULE_ast_topLevelStatement -> // + new StatementRootNode(node); + // case XLangParser.RULE_importAsDeclaration -> // - new ImportAsDeclarationNode(node); + new ImportDeclarationNode(node); case XLangParser.RULE_ast_importSource -> // new ImportSourceNode(node); - case XLangParser.RULE_qualifiedName -> // - new ImportQualifiedNameNode(node); - case XLangParser.RULE_program -> // - new ProgramNode(node); + // + case XLangParser.RULE_variableDeclaration -> // + new VariableDeclarationNode(node); + case XLangParser.RULE_blockStatement -> // + new BlockStatementNode(node); + // + case XLangParser.RULE_parameterizedTypeNode -> // + new ParameterizedTypeNode(node); + case XLangParser.RULE_arguments_ -> // + new CalleeArgumentsNode(node); + case XLangParser.RULE_identifier_ex -> // + new ObjectPropertyNode(node); + case XLangParser.RULE_propertyAssignment -> // + new ObjectPropertyAssignmentNode(node); + case XLangParser.RULE_expression_single -> // + new ExpressionElementNode(node); + // + case XLangParser.RULE_functionDeclaration -> // + new FunctionDeclarationNode(node); + case XLangParser.RULE_parameterDeclaration -> // + new FunctionParameterDeclarationNode(node); + case XLangParser.RULE_arrowFunctionExpression -> // + new ArrowFunctionNode(node); + case XLangParser.RULE_expression_functionBody -> // + new ArrowFunctionBodyNode(node); default -> new RuleSpecNode(node); }; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionBodyNode.java similarity index 62% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationNode.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionBodyNode.java index c0204965c..1b08a5fa5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportAsDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionBodyNode.java @@ -5,11 +5,11 @@ import org.jetbrains.annotations.NotNull; /** * @author flytreeleft - * @date 2025-06-29 + * @date 2025-06-30 */ -public class ImportAsDeclarationNode extends RuleSpecNode { +public class ArrowFunctionBodyNode extends RuleSpecNode { - public ImportAsDeclarationNode(@NotNull ASTNode node) { + public ArrowFunctionBodyNode(@NotNull ASTNode node) { super(node); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java new file mode 100644 index 000000000..14b6c9970 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 箭头函数节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class ArrowFunctionNode extends RuleSpecNode { + + public ArrowFunctionNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java new file mode 100644 index 000000000..ce10f11bd --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java @@ -0,0 +1,19 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * { ... } 块节点 + *

+ * 用于 tryif 和函数体等节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class BlockStatementNode extends RuleSpecNode { + + public BlockStatementNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java new file mode 100644 index 000000000..aef25e42f --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 函数调用的参数列表节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class CalleeArgumentsNode extends RuleSpecNode { + + public CalleeArgumentsNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionElementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionElementNode.java new file mode 100644 index 000000000..3d9bfcc60 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionElementNode.java @@ -0,0 +1,29 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 表达式中的元素节点 + *

+ * 其叶子节点可以为引用的变量名(identifier 类型),也可以为字面量(literal 类型)。 + * 其可以多层嵌套: + *

+ * 表达式 a.b.c(1, 2, 3) 除自身外,还包含以下元素
+ * - a
+ * - a.b
+ * - a.b.c
+ * - 1
+ * - 2
+ * - 3
+ * 
+ * + * @author flytreeleft + * @date 2025-06-30 + */ +public class ExpressionElementNode extends RuleSpecNode { + + public ExpressionElementNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportQualifiedNameNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java similarity index 62% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportQualifiedNameNode.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java index 28cf57285..2672a0e39 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportQualifiedNameNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java @@ -5,11 +5,11 @@ import org.jetbrains.annotations.NotNull; /** * @author flytreeleft - * @date 2025-06-29 + * @date 2025-06-30 */ -public class ImportQualifiedNameNode extends RuleSpecNode { +public class FunctionDeclarationNode extends RuleSpecNode { - public ImportQualifiedNameNode(@NotNull ASTNode node) { + public FunctionDeclarationNode(@NotNull ASTNode node) { super(node); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java new file mode 100644 index 000000000..477193f4e --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 函数参数定义节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class FunctionParameterDeclarationNode extends RuleSpecNode { + + public FunctionParameterDeclarationNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java new file mode 100644 index 000000000..d0a938856 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * import xxx; 节点(含结束符) + * + * @author flytreeleft + * @date 2025-06-29 + */ +public class ImportDeclarationNode extends RuleSpecNode { + + public ImportDeclarationNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java index 3b439ee68..22ec9ef15 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -7,6 +7,8 @@ import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassRe import org.jetbrains.annotations.NotNull; /** + * import 语句中导入的类名的节点 + * * @author flytreeleft * @date 2025-06-29 */ diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java new file mode 100644 index 000000000..800f03879 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java @@ -0,0 +1,19 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 对象的属性赋值节点 + *

+ * 如 {a, b: 1} 中,ab: 1 均为该类型节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class ObjectPropertyAssignmentNode extends RuleSpecNode { + + public ObjectPropertyAssignmentNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyNode.java new file mode 100644 index 000000000..99a55daf2 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyNode.java @@ -0,0 +1,20 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 对象的属性节点 + *

+ * 如 a.b.c(){b, c: 1} 中, + * bc 均为该类型节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class ObjectPropertyNode extends RuleSpecNode { + + public ObjectPropertyNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java new file mode 100644 index 000000000..209f26d44 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * new 语句中的类名节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class ParameterizedTypeNode extends RuleSpecNode { + + public ParameterizedTypeNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementRootNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementRootNode.java new file mode 100644 index 000000000..526252af9 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementRootNode.java @@ -0,0 +1,19 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 各种语句的根节点 + *

+ * 赋值、函数、导入、try 等语句,各自均有唯一的根节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class StatementRootNode extends RuleSpecNode { + + public StatementRootNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java new file mode 100644 index 000000000..70a46d77d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 含 constlet 语句 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class VariableDeclarationNode extends RuleSpecNode { + + public VariableDeclarationNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index 5072e2695..e3014539a 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -3,36 +3,66 @@ package io.nop.idea.plugin.lang; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.DebugUtil; -import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase; +import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.idea.plugin.lang.script.XLangScriptFileType; import org.jetbrains.annotations.NotNull; -import org.junit.Test; /** * @author flytreeleft * @date 2025-06-28 */ -public class TestXLangScriptParser extends LightPlatformCodeInsightFixture4TestCase { +public class TestXLangScriptParser extends BaseXLangPluginTestCase { private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); - @Test - public void testImportStatement() { - String code = "import io.nop.core.model.query.QueryBeanHelper;"; + public void testParseStatement() { +// checkMatchJavaParseTree("import java.lang.;", "/test/ast/statement-err-1.ast"); +// checkMatchJavaParseTree("const abc = ", "/test/ast/statement-err-2.ast"); +// checkMatchJavaParseTree("const abc = () =>", "/test/ast/statement-err-3.ast"); +// +// checkMatchJavaParseTree("import java.lang.String;", "/test/ast/statement-1.ast"); +// +// checkMatchJavaParseTree(""" +// import java.lang.String; +// import java.lang.Number; +// """, "/test/ast/statement-2.ast"); +// +// checkMatchJavaParseTree(""" +// import java.lang.String; +// import java.lang.Number; +// const abc = new String("abc"); +// const def = 123; +// """, "/test/ast/statement-3.ast"); +// +// checkMatchJavaParseTree(""" +// import java.lang.Number; +// const fn1 = (a, b) => a + b; +// function fn2(a, b) { +// return a + b; +// } +// function fn3(a: string, b: number) { +// return a + b; +// } +// """, "/test/ast/statement-4.ast"); - checkMatchJavaParseTree(code); + checkMatchJavaParseTree(""" + const abc = ormTemplate.findListByQuery(query, mapper); + query.addFilter(filter(query, svcCtx)); + a.b.c(1, 2, 3); + const def = {a, b: 1}; + """, // + "/test/ast/statement-5.ast"); } - protected void checkMatchJavaParseTree(String code) { + protected void checkMatchJavaParseTree(String code, String expectedAstFile) { PsiFile testFile = myFixture.configureByText("sample." + ext, code); String testTree = toParseTreeText(testFile); - PsiFile javaFile = myFixture.configureByText("sample.java", code); - String javaTree = toParseTreeText(javaFile); + String expectedTree = readVfsResource(expectedAstFile); - assertEquals(javaTree, testTree); + assertEquals(expectedTree, testTree); } protected String toParseTreeText(@NotNull PsiElement file) { - return DebugUtil.psiToString(file, false, false); + return DebugUtil.psiToString(file, false, true); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast new file mode 100644 index 000000000..18c93259f --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast @@ -0,0 +1,24 @@ +FILE(0,24) + ProgramNode(program)(0,24) + RuleSpecNode(topLevelStatements_)(0,24) + RuleSpecNode(ast_topLevelStatement)(0,24) + RuleSpecNode(moduleDeclaration_import)(0,24) + ImportAsDeclarationNode(importAsDeclaration)(0,24) + PsiElement('import')('import')(0,6) + ImportSourceNode(ast_importSource)(7,23) + ImportQualifiedNameNode(qualifiedName)(7,23) + RuleSpecNode(qualifiedName_name_)(7,11) + RuleSpecNode(identifier)(7,11) + PsiElement(Identifier)('java')(7,11) + PsiElement('.')('.')(11,12) + ImportQualifiedNameNode(qualifiedName)(12,23) + RuleSpecNode(qualifiedName_name_)(12,16) + RuleSpecNode(identifier)(12,16) + PsiElement(Identifier)('lang')(12,16) + PsiElement('.')('.')(16,17) + ImportQualifiedNameNode(qualifiedName)(17,23) + RuleSpecNode(qualifiedName_name_)(17,23) + RuleSpecNode(identifier)(17,23) + PsiElement(Identifier)('String')(17,23) + RuleSpecNode(eos__)(23,24) + PsiElement(';')(';')(23,24) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-2.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-2.ast new file mode 100644 index 000000000..d97bde885 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-2.ast @@ -0,0 +1,45 @@ +FILE(0,50) + ProgramNode(program)(0,49) + RuleSpecNode(topLevelStatements_)(0,49) + RuleSpecNode(ast_topLevelStatement)(0,24) + RuleSpecNode(moduleDeclaration_import)(0,24) + ImportAsDeclarationNode(importAsDeclaration)(0,24) + PsiElement('import')('import')(0,6) + ImportSourceNode(ast_importSource)(7,23) + ImportQualifiedNameNode(qualifiedName)(7,23) + RuleSpecNode(qualifiedName_name_)(7,11) + RuleSpecNode(identifier)(7,11) + PsiElement(Identifier)('java')(7,11) + PsiElement('.')('.')(11,12) + ImportQualifiedNameNode(qualifiedName)(12,23) + RuleSpecNode(qualifiedName_name_)(12,16) + RuleSpecNode(identifier)(12,16) + PsiElement(Identifier)('lang')(12,16) + PsiElement('.')('.')(16,17) + ImportQualifiedNameNode(qualifiedName)(17,23) + RuleSpecNode(qualifiedName_name_)(17,23) + RuleSpecNode(identifier)(17,23) + PsiElement(Identifier)('String')(17,23) + RuleSpecNode(eos__)(23,24) + PsiElement(';')(';')(23,24) + RuleSpecNode(ast_topLevelStatement)(25,49) + RuleSpecNode(moduleDeclaration_import)(25,49) + ImportAsDeclarationNode(importAsDeclaration)(25,49) + PsiElement('import')('import')(25,31) + ImportSourceNode(ast_importSource)(32,48) + ImportQualifiedNameNode(qualifiedName)(32,48) + RuleSpecNode(qualifiedName_name_)(32,36) + RuleSpecNode(identifier)(32,36) + PsiElement(Identifier)('java')(32,36) + PsiElement('.')('.')(36,37) + ImportQualifiedNameNode(qualifiedName)(37,48) + RuleSpecNode(qualifiedName_name_)(37,41) + RuleSpecNode(identifier)(37,41) + PsiElement(Identifier)('lang')(37,41) + PsiElement('.')('.')(41,42) + ImportQualifiedNameNode(qualifiedName)(42,48) + RuleSpecNode(qualifiedName_name_)(42,48) + RuleSpecNode(identifier)(42,48) + PsiElement(Identifier)('Number')(42,48) + RuleSpecNode(eos__)(48,49) + PsiElement(';')(';')(48,49) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-3.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-3.ast new file mode 100644 index 000000000..15264d2c3 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-3.ast @@ -0,0 +1,92 @@ +FILE(0,98) + ProgramNode(program)(0,97) + RuleSpecNode(topLevelStatements_)(0,97) + RuleSpecNode(ast_topLevelStatement)(0,24) + RuleSpecNode(moduleDeclaration_import)(0,24) + ImportAsDeclarationNode(importAsDeclaration)(0,24) + PsiElement('import')('import')(0,6) + ImportSourceNode(ast_importSource)(7,23) + ImportQualifiedNameNode(qualifiedName)(7,23) + RuleSpecNode(qualifiedName_name_)(7,11) + RuleSpecNode(identifier)(7,11) + PsiElement(Identifier)('java')(7,11) + PsiElement('.')('.')(11,12) + ImportQualifiedNameNode(qualifiedName)(12,23) + RuleSpecNode(qualifiedName_name_)(12,16) + RuleSpecNode(identifier)(12,16) + PsiElement(Identifier)('lang')(12,16) + PsiElement('.')('.')(16,17) + ImportQualifiedNameNode(qualifiedName)(17,23) + RuleSpecNode(qualifiedName_name_)(17,23) + RuleSpecNode(identifier)(17,23) + PsiElement(Identifier)('String')(17,23) + RuleSpecNode(eos__)(23,24) + PsiElement(';')(';')(23,24) + RuleSpecNode(ast_topLevelStatement)(25,49) + RuleSpecNode(moduleDeclaration_import)(25,49) + ImportAsDeclarationNode(importAsDeclaration)(25,49) + PsiElement('import')('import')(25,31) + ImportSourceNode(ast_importSource)(32,48) + ImportQualifiedNameNode(qualifiedName)(32,48) + RuleSpecNode(qualifiedName_name_)(32,36) + RuleSpecNode(identifier)(32,36) + PsiElement(Identifier)('java')(32,36) + PsiElement('.')('.')(36,37) + ImportQualifiedNameNode(qualifiedName)(37,48) + RuleSpecNode(qualifiedName_name_)(37,41) + RuleSpecNode(identifier)(37,41) + PsiElement(Identifier)('lang')(37,41) + PsiElement('.')('.')(41,42) + ImportQualifiedNameNode(qualifiedName)(42,48) + RuleSpecNode(qualifiedName_name_)(42,48) + RuleSpecNode(identifier)(42,48) + PsiElement(Identifier)('Number')(42,48) + RuleSpecNode(eos__)(48,49) + PsiElement(';')(';')(48,49) + RuleSpecNode(ast_topLevelStatement)(50,80) + RuleSpecNode(statement)(50,80) + RuleSpecNode(variableDeclaration)(50,80) + RuleSpecNode(varModifier_)(50,55) + PsiElement('const')('const')(50,55) + RuleSpecNode(variableDeclarators_)(56,79) + RuleSpecNode(variableDeclarator)(56,79) + RuleSpecNode(ast_identifierOrPattern)(56,59) + RuleSpecNode(identifier)(56,59) + PsiElement(Identifier)('abc')(56,59) + RuleSpecNode(expression_initializer)(60,79) + PsiElement('=')('=')(60,61) + RuleSpecNode(expression_single)(62,79) + PsiElement('new')('new')(62,65) + RuleSpecNode(parameterizedTypeNode)(66,72) + RuleSpecNode(qualifiedName_)(66,72) + ImportQualifiedNameNode(qualifiedName)(66,72) + RuleSpecNode(qualifiedName_name_)(66,72) + RuleSpecNode(identifier)(66,72) + PsiElement(Identifier)('String')(66,72) + RuleSpecNode(arguments_)(72,79) + PsiElement('(')('(')(72,73) + RuleSpecNode(expression_single)(73,78) + RuleSpecNode(literal)(73,78) + RuleSpecNode(literal_string)(73,78) + PsiElement(StringLiteral)('"abc"')(73,78) + PsiElement(')')(')')(78,79) + RuleSpecNode(eos__)(79,80) + PsiElement(';')(';')(79,80) + RuleSpecNode(ast_topLevelStatement)(81,97) + RuleSpecNode(statement)(81,97) + RuleSpecNode(variableDeclaration)(81,97) + RuleSpecNode(varModifier_)(81,86) + PsiElement('const')('const')(81,86) + RuleSpecNode(variableDeclarators_)(87,96) + RuleSpecNode(variableDeclarator)(87,96) + RuleSpecNode(ast_identifierOrPattern)(87,90) + RuleSpecNode(identifier)(87,90) + PsiElement(Identifier)('def')(87,90) + RuleSpecNode(expression_initializer)(91,96) + PsiElement('=')('=')(91,92) + RuleSpecNode(expression_single)(93,96) + RuleSpecNode(literal)(93,96) + RuleSpecNode(literal_numeric)(93,96) + PsiElement(DecimalIntegerLiteral)('123')(93,96) + RuleSpecNode(eos__)(96,97) + PsiElement(';')(';')(96,97) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-4.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-4.ast new file mode 100644 index 000000000..9b51459c7 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-4.ast @@ -0,0 +1,142 @@ +FILE(0,152) + ProgramNode(program)(0,151) + RuleSpecNode(topLevelStatements_)(0,151) + RuleSpecNode(ast_topLevelStatement)(0,24) + RuleSpecNode(moduleDeclaration_import)(0,24) + ImportAsDeclarationNode(importAsDeclaration)(0,24) + PsiElement('import')('import')(0,6) + ImportSourceNode(ast_importSource)(7,23) + ImportQualifiedNameNode(qualifiedName)(7,23) + RuleSpecNode(qualifiedName_name_)(7,11) + RuleSpecNode(identifier)(7,11) + PsiElement(Identifier)('java')(7,11) + PsiElement('.')('.')(11,12) + ImportQualifiedNameNode(qualifiedName)(12,23) + RuleSpecNode(qualifiedName_name_)(12,16) + RuleSpecNode(identifier)(12,16) + PsiElement(Identifier)('lang')(12,16) + PsiElement('.')('.')(16,17) + ImportQualifiedNameNode(qualifiedName)(17,23) + RuleSpecNode(qualifiedName_name_)(17,23) + RuleSpecNode(identifier)(17,23) + PsiElement(Identifier)('Number')(17,23) + RuleSpecNode(eos__)(23,24) + PsiElement(';')(';')(23,24) + RuleSpecNode(ast_topLevelStatement)(25,53) + RuleSpecNode(statement)(25,53) + RuleSpecNode(variableDeclaration)(25,53) + RuleSpecNode(varModifier_)(25,30) + PsiElement('const')('const')(25,30) + RuleSpecNode(variableDeclarators_)(31,52) + RuleSpecNode(variableDeclarator)(31,52) + RuleSpecNode(ast_identifierOrPattern)(31,34) + RuleSpecNode(identifier)(31,34) + PsiElement(Identifier)('fn1')(31,34) + RuleSpecNode(expression_initializer)(35,52) + PsiElement('=')('=')(35,36) + RuleSpecNode(expression_single)(37,52) + RuleSpecNode(arrowFunctionExpression)(37,52) + PsiElement('(')('(')(37,38) + RuleSpecNode(parameterList_)(38,42) + RuleSpecNode(parameterDeclaration)(38,39) + RuleSpecNode(ast_identifierOrPattern)(38,39) + RuleSpecNode(identifier)(38,39) + PsiElement(Identifier)('a')(38,39) + PsiElement(',')(',')(39,40) + RuleSpecNode(parameterDeclaration)(41,42) + RuleSpecNode(ast_identifierOrPattern)(41,42) + RuleSpecNode(identifier)(41,42) + PsiElement(Identifier)('b')(41,42) + PsiElement(')')(')')(42,43) + PsiElement('=>')('=>')(44,46) + RuleSpecNode(expression_functionBody)(47,52) + RuleSpecNode(expression_single)(47,52) + RuleSpecNode(expression_single)(47,48) + RuleSpecNode(identifier)(47,48) + PsiElement(Identifier)('a')(47,48) + PsiElement('+')('+')(49,50) + RuleSpecNode(expression_single)(51,52) + RuleSpecNode(identifier)(51,52) + PsiElement(Identifier)('b')(51,52) + RuleSpecNode(eos__)(52,53) + PsiElement(';')(';')(52,53) + RuleSpecNode(ast_topLevelStatement)(54,94) + RuleSpecNode(statement)(54,94) + RuleSpecNode(functionDeclaration)(54,94) + PsiElement('function')('function')(54,62) + RuleSpecNode(identifier)(63,66) + PsiElement(Identifier)('fn2')(63,66) + PsiElement('(')('(')(66,67) + RuleSpecNode(parameterList_)(67,71) + RuleSpecNode(parameterDeclaration)(67,68) + RuleSpecNode(ast_identifierOrPattern)(67,68) + RuleSpecNode(identifier)(67,68) + PsiElement(Identifier)('a')(67,68) + PsiElement(',')(',')(68,69) + RuleSpecNode(parameterDeclaration)(70,71) + RuleSpecNode(ast_identifierOrPattern)(70,71) + RuleSpecNode(identifier)(70,71) + PsiElement(Identifier)('b')(70,71) + PsiElement(')')(')')(71,72) + RuleSpecNode(blockStatement)(73,94) + PsiElement('{')('{')(73,74) + RuleSpecNode(statements_)(79,92) + RuleSpecNode(statement)(79,92) + RuleSpecNode(returnStatement)(79,92) + PsiElement('return')('return')(79,85) + RuleSpecNode(expression_single)(86,91) + RuleSpecNode(expression_single)(86,87) + RuleSpecNode(identifier)(86,87) + PsiElement(Identifier)('a')(86,87) + PsiElement('+')('+')(88,89) + RuleSpecNode(expression_single)(90,91) + RuleSpecNode(identifier)(90,91) + PsiElement(Identifier)('b')(90,91) + RuleSpecNode(eos__)(91,92) + PsiElement(';')(';')(91,92) + PsiElement('}')('}')(93,94) + RuleSpecNode(ast_topLevelStatement)(95,151) + RuleSpecNode(statement)(95,151) + RuleSpecNode(functionDeclaration)(95,151) + PsiElement('function')('function')(95,103) + RuleSpecNode(identifier)(104,107) + PsiElement(Identifier)('fn3')(104,107) + PsiElement('(')('(')(107,108) + RuleSpecNode(parameterList_)(108,128) + RuleSpecNode(parameterDeclaration)(108,117) + RuleSpecNode(ast_identifierOrPattern)(108,109) + RuleSpecNode(identifier)(108,109) + PsiElement(Identifier)('a')(108,109) + RuleSpecNode(namedTypeNode_annotation)(109,117) + PsiElement(':')(':')(109,110) + RuleSpecNode(namedTypeNode)(111,117) + RuleSpecNode(typeNameNode_predefined)(111,117) + PsiElement('string')('string')(111,117) + PsiElement(',')(',')(117,118) + RuleSpecNode(parameterDeclaration)(119,128) + RuleSpecNode(ast_identifierOrPattern)(119,120) + RuleSpecNode(identifier)(119,120) + PsiElement(Identifier)('b')(119,120) + RuleSpecNode(namedTypeNode_annotation)(120,128) + PsiElement(':')(':')(120,121) + RuleSpecNode(namedTypeNode)(122,128) + RuleSpecNode(typeNameNode_predefined)(122,128) + PsiElement('number')('number')(122,128) + PsiElement(')')(')')(128,129) + RuleSpecNode(blockStatement)(130,151) + PsiElement('{')('{')(130,131) + RuleSpecNode(statements_)(136,149) + RuleSpecNode(statement)(136,149) + RuleSpecNode(returnStatement)(136,149) + PsiElement('return')('return')(136,142) + RuleSpecNode(expression_single)(143,148) + RuleSpecNode(expression_single)(143,144) + RuleSpecNode(identifier)(143,144) + PsiElement(Identifier)('a')(143,144) + PsiElement('+')('+')(145,146) + RuleSpecNode(expression_single)(147,148) + RuleSpecNode(identifier)(147,148) + PsiElement(Identifier)('b')(147,148) + RuleSpecNode(eos__)(148,149) + PsiElement(';')(';')(148,149) + PsiElement('}')('}')(150,151) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast new file mode 100644 index 000000000..cc1adfd29 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast @@ -0,0 +1,145 @@ +FILE(0,135) + ProgramNode(program)(0,134) + RuleSpecNode(topLevelStatements_)(0,134) + StatementRootNode(ast_topLevelStatement)(0,55) + RuleSpecNode(statement)(0,55) + VariableDeclarationNode(variableDeclaration)(0,55) + RuleSpecNode(varModifier_)(0,5) + PsiElement('const')('const')(0,5) + RuleSpecNode(variableDeclarators_)(6,54) + RuleSpecNode(variableDeclarator)(6,54) + RuleSpecNode(ast_identifierOrPattern)(6,9) + RuleSpecNode(identifier)(6,9) + PsiElement(Identifier)('abc')(6,9) + RuleSpecNode(expression_initializer)(10,54) + PsiElement('=')('=')(10,11) + RuleSpecNode(expression_single)(12,54) + RuleSpecNode(expression_single)(12,39) + RuleSpecNode(expression_single)(12,23) + RuleSpecNode(identifier)(12,23) + PsiElement(Identifier)('ormTemplate')(12,23) + PsiElement('.')('.')(23,24) + ObjectPropertyNode(identifier_ex)(24,39) + RuleSpecNode(identifierOrKeyword_)(24,39) + RuleSpecNode(identifier)(24,39) + PsiElement(Identifier)('findListByQuery')(24,39) + CalleeArgumentsNode(arguments_)(39,54) + PsiElement('(')('(')(39,40) + RuleSpecNode(expression_single)(40,45) + RuleSpecNode(identifier)(40,45) + PsiElement(Identifier)('query')(40,45) + PsiElement(',')(',')(45,46) + RuleSpecNode(expression_single)(47,53) + RuleSpecNode(identifier)(47,53) + PsiElement(Identifier)('mapper')(47,53) + PsiElement(')')(')')(53,54) + RuleSpecNode(eos__)(54,55) + PsiElement(';')(';')(54,55) + StatementRootNode(ast_topLevelStatement)(56,95) + RuleSpecNode(statement)(56,95) + RuleSpecNode(expressionStatement)(56,95) + RuleSpecNode(expression_single)(56,94) + RuleSpecNode(expression_single)(56,71) + RuleSpecNode(expression_single)(56,61) + RuleSpecNode(identifier)(56,61) + PsiElement(Identifier)('query')(56,61) + PsiElement('.')('.')(61,62) + ObjectPropertyNode(identifier_ex)(62,71) + RuleSpecNode(identifierOrKeyword_)(62,71) + RuleSpecNode(identifier)(62,71) + PsiElement(Identifier)('addFilter')(62,71) + CalleeArgumentsNode(arguments_)(71,94) + PsiElement('(')('(')(71,72) + RuleSpecNode(expression_single)(72,93) + RuleSpecNode(expression_single)(72,78) + RuleSpecNode(identifier)(72,78) + PsiElement(Identifier)('filter')(72,78) + CalleeArgumentsNode(arguments_)(78,93) + PsiElement('(')('(')(78,79) + RuleSpecNode(expression_single)(79,84) + RuleSpecNode(identifier)(79,84) + PsiElement(Identifier)('query')(79,84) + PsiElement(',')(',')(84,85) + RuleSpecNode(expression_single)(86,92) + RuleSpecNode(identifier)(86,92) + PsiElement(Identifier)('svcCtx')(86,92) + PsiElement(')')(')')(92,93) + PsiElement(')')(')')(93,94) + RuleSpecNode(eos__)(94,95) + PsiElement(';')(';')(94,95) + StatementRootNode(ast_topLevelStatement)(96,111) + RuleSpecNode(statement)(96,111) + RuleSpecNode(expressionStatement)(96,111) + RuleSpecNode(expression_single)(96,110) + RuleSpecNode(expression_single)(96,101) + RuleSpecNode(expression_single)(96,99) + RuleSpecNode(expression_single)(96,97) + RuleSpecNode(identifier)(96,97) + PsiElement(Identifier)('a')(96,97) + PsiElement('.')('.')(97,98) + ObjectPropertyNode(identifier_ex)(98,99) + RuleSpecNode(identifierOrKeyword_)(98,99) + RuleSpecNode(identifier)(98,99) + PsiElement(Identifier)('b')(98,99) + PsiElement('.')('.')(99,100) + ObjectPropertyNode(identifier_ex)(100,101) + RuleSpecNode(identifierOrKeyword_)(100,101) + RuleSpecNode(identifier)(100,101) + PsiElement(Identifier)('c')(100,101) + CalleeArgumentsNode(arguments_)(101,110) + PsiElement('(')('(')(101,102) + RuleSpecNode(expression_single)(102,103) + RuleSpecNode(literal)(102,103) + RuleSpecNode(literal_numeric)(102,103) + PsiElement(DecimalIntegerLiteral)('1')(102,103) + PsiElement(',')(',')(103,104) + RuleSpecNode(expression_single)(105,106) + RuleSpecNode(literal)(105,106) + RuleSpecNode(literal_numeric)(105,106) + PsiElement(DecimalIntegerLiteral)('2')(105,106) + PsiElement(',')(',')(106,107) + RuleSpecNode(expression_single)(108,109) + RuleSpecNode(literal)(108,109) + RuleSpecNode(literal_numeric)(108,109) + PsiElement(DecimalIntegerLiteral)('3')(108,109) + PsiElement(')')(')')(109,110) + RuleSpecNode(eos__)(110,111) + PsiElement(';')(';')(110,111) + StatementRootNode(ast_topLevelStatement)(112,134) + RuleSpecNode(statement)(112,134) + VariableDeclarationNode(variableDeclaration)(112,134) + RuleSpecNode(varModifier_)(112,117) + PsiElement('const')('const')(112,117) + RuleSpecNode(variableDeclarators_)(118,133) + RuleSpecNode(variableDeclarator)(118,133) + RuleSpecNode(ast_identifierOrPattern)(118,121) + RuleSpecNode(identifier)(118,121) + PsiElement(Identifier)('def')(118,121) + RuleSpecNode(expression_initializer)(122,133) + PsiElement('=')('=')(122,123) + RuleSpecNode(expression_single)(124,133) + RuleSpecNode(objectExpression)(124,133) + PsiElement('{')('{')(124,125) + RuleSpecNode(objectProperties_)(125,132) + RuleSpecNode(ast_objectProperty)(125,126) + RuleSpecNode(propertyAssignment)(125,126) + ObjectPropertyNode(identifier_ex)(125,126) + RuleSpecNode(identifierOrKeyword_)(125,126) + RuleSpecNode(identifier)(125,126) + PsiElement(Identifier)('a')(125,126) + PsiElement(',')(',')(126,127) + RuleSpecNode(ast_objectProperty)(128,132) + RuleSpecNode(propertyAssignment)(128,132) + RuleSpecNode(expression_propName)(128,129) + ObjectPropertyNode(identifier_ex)(128,129) + RuleSpecNode(identifierOrKeyword_)(128,129) + RuleSpecNode(identifier)(128,129) + PsiElement(Identifier)('b')(128,129) + PsiElement(':')(':')(129,130) + RuleSpecNode(expression_single)(131,132) + RuleSpecNode(literal)(131,132) + RuleSpecNode(literal_numeric)(131,132) + PsiElement(DecimalIntegerLiteral)('1')(131,132) + PsiElement('}')('}')(132,133) + RuleSpecNode(eos__)(133,134) + PsiElement(';')(';')(133,134) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast new file mode 100644 index 000000000..980195c60 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast @@ -0,0 +1,25 @@ +FILE(0,18) + ProgramNode(program)(0,18) + RuleSpecNode(topLevelStatements_)(0,18) + RuleSpecNode(ast_topLevelStatement)(0,17) + RuleSpecNode(moduleDeclaration_import)(0,17) + ImportAsDeclarationNode(importAsDeclaration)(0,17) + PsiElement('import')('import')(0,6) + ImportSourceNode(ast_importSource)(7,16) + ImportQualifiedNameNode(qualifiedName)(7,16) + RuleSpecNode(qualifiedName_name_)(7,11) + RuleSpecNode(identifier)(7,11) + PsiElement(Identifier)('java')(7,11) + PsiElement('.')('.')(11,12) + ImportQualifiedNameNode(qualifiedName)(12,16) + RuleSpecNode(qualifiedName_name_)(12,16) + RuleSpecNode(identifier)(12,16) + PsiElement(Identifier)('lang')(12,16) + RuleSpecNode(eos__)(16,17) + PsiErrorElement:rule eos__ failed predicate: {this.lineTerminatorAhead()}? +(16,17) + PsiElement('.')('.')(16,17) + RuleSpecNode(ast_topLevelStatement)(17,18) + RuleSpecNode(statement)(17,18) + RuleSpecNode(emptyStatement)(17,18) + PsiElement(';')(';')(17,18) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-2.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-2.ast new file mode 100644 index 000000000..e86f49e99 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-2.ast @@ -0,0 +1,21 @@ +FILE(0,12) + ProgramNode(program)(0,12) + RuleSpecNode(topLevelStatements_)(0,12) + RuleSpecNode(ast_topLevelStatement)(0,12) + RuleSpecNode(statement)(0,12) + RuleSpecNode(variableDeclaration)(0,12) + RuleSpecNode(varModifier_)(0,5) + PsiElement('const')('const')(0,5) + RuleSpecNode(variableDeclarators_)(6,12) + RuleSpecNode(variableDeclarator)(6,12) + RuleSpecNode(ast_identifierOrPattern)(6,9) + RuleSpecNode(identifier)(6,9) + PsiElement(Identifier)('abc')(6,9) + RuleSpecNode(expression_initializer)(10,12) + PsiElement('=')('=')(10,11) + RuleSpecNode(expression_single)(12,12) + PsiErrorElement:mismatched input '' expecting {RegularExpressionLiteral, '[', '(', '{', '++', '--', '+', '-', '~', '!', 'null', BooleanLiteral, DecimalIntegerLiteral, HexIntegerLiteral, BinaryIntegerLiteral, DecimalLiteral, 'typeof', 'new', 'this', 'from', 'super', 'type', StringLiteral, TemplateStringLiteral, Identifier, '#{'} +(12,12) + + RuleSpecNode(eos__)(12,12) + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-3.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-3.ast new file mode 100644 index 000000000..3185c842c --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-3.ast @@ -0,0 +1,26 @@ +FILE(0,17) + ProgramNode(program)(0,17) + RuleSpecNode(topLevelStatements_)(0,17) + RuleSpecNode(ast_topLevelStatement)(0,17) + RuleSpecNode(statement)(0,17) + RuleSpecNode(variableDeclaration)(0,17) + RuleSpecNode(varModifier_)(0,5) + PsiElement('const')('const')(0,5) + RuleSpecNode(variableDeclarators_)(6,17) + RuleSpecNode(variableDeclarator)(6,17) + RuleSpecNode(ast_identifierOrPattern)(6,9) + RuleSpecNode(identifier)(6,9) + PsiElement(Identifier)('abc')(6,9) + RuleSpecNode(expression_initializer)(10,17) + PsiElement('=')('=')(10,11) + RuleSpecNode(expression_single)(12,17) + RuleSpecNode(arrowFunctionExpression)(12,17) + PsiElement('(')('(')(12,13) + PsiElement(')')(')')(13,14) + PsiElement('=>')('=>')(15,17) + RuleSpecNode(expression_functionBody)(17,17) + PsiErrorElement:mismatched input '' expecting {RegularExpressionLiteral, '[', '(', '{', '++', '--', '+', '-', '~', '!', 'null', BooleanLiteral, DecimalIntegerLiteral, HexIntegerLiteral, BinaryIntegerLiteral, DecimalLiteral, 'typeof', 'new', 'this', 'from', 'super', 'type', StringLiteral, TemplateStringLiteral, Identifier, '#{'} +(17,17) + + RuleSpecNode(eos__)(17,17) + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index 107202d42..b42753f67 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -13,7 +13,6 @@ Date: Wed, 2 Jul 2025 21:42:21 +0800 Subject: [PATCH 30/82] =?UTF-8?q?nop-idea-pugin:=20=E5=B0=9D=E8=AF=95?= =?UTF-8?q?=E6=9E=84=E9=80=A0=20XLang=20Script=20=E4=B8=AD=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E6=88=90=E5=91=98=E7=9A=84=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/script/XLangScriptASTFactory.java | 4 +- .../lang/script/XLangScriptParserAdaptor.java | 1 + .../script/XLangScriptParserDefinition.java | 22 +- .../lang/script/psi/ExpressionNode.java | 250 +++++++++++++ .../script/psi/FunctionDeclarationNode.java | 2 + .../plugin/lang/script/psi/Identifier.java | 20 ++ .../lang/script/psi/IdentifierNode.java | 20 +- .../lang/script/psi/ImportSourceNode.java | 12 +- .../plugin/lang/script/psi/LiteralNode.java | 48 +++ ...ntNode.java => ObjectDeclarationNode.java} | 19 +- ...ropertyNode.java => ObjectMemberNode.java} | 6 +- .../psi/ObjectPropertyDeclarationNode.java | 20 ++ .../plugin/lang/script/psi/RuleSpecNode.java | 16 +- .../reference/ClassMethodReference.java | 33 ++ .../reference/ClassPropertyReference.java | 30 ++ .../plugin/lang/TestXLangScriptParser.java | 88 +++-- .../lang/TestXLangScriptReferences.java | 50 ++- .../resources/_vfs/test/ast/statement-5.ast | 335 ++++++++++-------- 18 files changed, 757 insertions(+), 219 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/{ExpressionElementNode.java => ObjectDeclarationNode.java} (36%) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/{ObjectPropertyNode.java => ObjectMemberNode.java} (70%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java index da8c51f00..5be34063b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java @@ -4,7 +4,7 @@ import com.intellij.lang.ASTFactory; import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.tree.IElementType; -import io.nop.idea.plugin.lang.script.psi.IdentifierNode; +import io.nop.idea.plugin.lang.script.psi.Identifier; import io.nop.xlang.parse.antlr.XLangLexer; import org.antlr.intellij.adaptor.lexer.TokenIElementType; import org.jetbrains.annotations.NotNull; @@ -27,7 +27,7 @@ public class XLangScriptASTFactory extends ASTFactory { } return switch (token.getANTLRTokenType()) { - case XLangLexer.Identifier -> new IdentifierNode(token, text); + case XLangLexer.Identifier -> new Identifier(token, text); default -> new LeafPsiElement(token, text); }; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java index 51af0b8b4..477c77d83 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java @@ -19,6 +19,7 @@ public class XLangScriptParserAdaptor extends ANTLRParserAdaptor { @Override protected ParseTree parse(Parser parser, IElementType root) { + // TODO 为 dot 节点之后的空白添加占位节点,以便于触发代码补全 if (root instanceof IFileElementType) { return ((XLangParser) parser).program(); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index f289ae117..91885cdce 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -16,13 +16,17 @@ import io.nop.idea.plugin.lang.script.psi.ArrowFunctionBodyNode; import io.nop.idea.plugin.lang.script.psi.ArrowFunctionNode; import io.nop.idea.plugin.lang.script.psi.BlockStatementNode; import io.nop.idea.plugin.lang.script.psi.CalleeArgumentsNode; -import io.nop.idea.plugin.lang.script.psi.ExpressionElementNode; +import io.nop.idea.plugin.lang.script.psi.ExpressionNode; import io.nop.idea.plugin.lang.script.psi.FunctionDeclarationNode; import io.nop.idea.plugin.lang.script.psi.FunctionParameterDeclarationNode; +import io.nop.idea.plugin.lang.script.psi.IdentifierNode; import io.nop.idea.plugin.lang.script.psi.ImportDeclarationNode; import io.nop.idea.plugin.lang.script.psi.ImportSourceNode; +import io.nop.idea.plugin.lang.script.psi.LiteralNode; +import io.nop.idea.plugin.lang.script.psi.ObjectDeclarationNode; +import io.nop.idea.plugin.lang.script.psi.ObjectMemberNode; import io.nop.idea.plugin.lang.script.psi.ObjectPropertyAssignmentNode; -import io.nop.idea.plugin.lang.script.psi.ObjectPropertyNode; +import io.nop.idea.plugin.lang.script.psi.ObjectPropertyDeclarationNode; import io.nop.idea.plugin.lang.script.psi.ParameterizedTypeNode; import io.nop.idea.plugin.lang.script.psi.ProgramNode; import io.nop.idea.plugin.lang.script.psi.RuleSpecNode; @@ -93,6 +97,10 @@ public class XLangScriptParserDefinition implements ParserDefinition { new ProgramNode(node); case XLangParser.RULE_ast_topLevelStatement -> // new StatementRootNode(node); + case XLangParser.RULE_identifier -> // + new IdentifierNode(node); + case XLangParser.RULE_literal -> // + new LiteralNode(node); // case XLangParser.RULE_importAsDeclaration -> // new ImportDeclarationNode(node); @@ -104,16 +112,20 @@ public class XLangScriptParserDefinition implements ParserDefinition { case XLangParser.RULE_blockStatement -> // new BlockStatementNode(node); // + case XLangParser.RULE_expression_single -> // + new ExpressionNode(node); + case XLangParser.RULE_objectExpression -> // + new ObjectDeclarationNode(node); + case XLangParser.RULE_ast_objectProperty -> // + new ObjectPropertyDeclarationNode(node); case XLangParser.RULE_parameterizedTypeNode -> // new ParameterizedTypeNode(node); case XLangParser.RULE_arguments_ -> // new CalleeArgumentsNode(node); case XLangParser.RULE_identifier_ex -> // - new ObjectPropertyNode(node); + new ObjectMemberNode(node); case XLangParser.RULE_propertyAssignment -> // new ObjectPropertyAssignmentNode(node); - case XLangParser.RULE_expression_single -> // - new ExpressionElementNode(node); // case XLangParser.RULE_functionDeclaration -> // new FunctionDeclarationNode(node); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java new file mode 100644 index 000000000..156874b84 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -0,0 +1,250 @@ +package io.nop.idea.plugin.lang.script.psi; + +import java.util.Collection; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiType; +import com.intellij.psi.impl.PsiClassImplUtil; +import com.intellij.psi.util.PsiTreeUtil; +import io.nop.idea.plugin.utils.PsiClassHelper; +import org.jetbrains.annotations.NotNull; + +/** + * 表达式节点 + *

+ * {@link ObjectMemberNode 对象成员访问表达式} + * a.b.ca.b().c(), + * 以及{@link ObjectDeclarationNode 对象声明表达式} + * {a, b: 2} 均从属于该类型节点 + *

+ * 其叶子节点可以为引用的变量名(identifier 类型),也可以为字面量(literal 类型)。 + * 其可以多层嵌套,例如,表达式 a.b.c(1, 2, 3) 包含以下子表达式: + *

+ * - a
+ * - a.b
+ * - a.b.c
+ * - 1
+ * - 2
+ * - 3
+ * 
+ * + * @author flytreeleft + * @date 2025-06-30 + */ +public class ExpressionNode extends RuleSpecNode { + + public ExpressionNode(@NotNull ASTNode node) { + super(node); + } + + /** 是否为标识符 */ + public boolean isIdentifier() { + return getFirstChild() instanceof IdentifierNode; + } + + /** 是否为字面量 */ + public boolean isLiteral() { + return getFirstChild() instanceof LiteralNode; + } + + /** 是否为对象成员(成员变量或方法)表达式 */ + public boolean isObjectMember() { + return getFirstChild() instanceof ObjectMemberNode; + } + + /** + * 是否为对象声明表达式 + *

+ * {@link ObjectDeclarationNode} 的直接父节点为 {@link ExpressionNode}, + * 因此,在构造 {@link ObjectMemberNode} 的成员引用时, + * 一般不为对象声明中的属性构造引用 + */ + public boolean isObjectDeclaration() { + return getFirstChild() instanceof ObjectDeclarationNode; + } + + /** 是否为对象方法调用表达式 */ + public boolean isObjectMethodCall() { + return getLastChild() instanceof CalleeArgumentsNode; + } + + @Override + public PsiReference @NotNull [] doGetReferences() { + if (isObjectDeclaration()) { + return PsiReference.EMPTY_ARRAY; + } // + else if (isObjectMethodCall()) { +// // Note: 需加上相对于当前表达式的对象偏移量 +// TextRange calleeMethodTextRange = callee.getObjectMemberTextRange().shiftLeft(callee.getStartOffsetInParent()); + return PsiReference.EMPTY_ARRAY; + } + + return PsiReference.EMPTY_ARRAY; + } + + /** + * 获取调用方的方法 + *

+ * 注意,只有通过参数列表才能唯一确定调用方的方法 + */ + protected PsiMethod getCalleeMethod() { + ExpressionNode callee = (ExpressionNode) getFirstChild(); + + /* 函数调用,如 a(1): + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(')')(')') + */ + if (callee.isIdentifier()) { + return null; + } + + /* 对象方法调用,如 a.b(1): + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(')')(')') + */ + if (!callee.isObjectMember()) { + return null; + } + + PsiMethod[] calleeMethods = callee.getObjectMethods(); + PsiClass[] calleeMethodArgTypes = getCalleeArgumentTypes(); + + for (PsiMethod method : calleeMethods) { + if (matchMethodArgs(method, calleeMethodArgTypes)) { + return method; + } + } + return null; + } + + /** 获取对象成员在当前表达式中的 {@link TextRange} */ + protected TextRange getObjectMemberTextRange() { + ObjectMemberNode member = (ObjectMemberNode) getLastChild(); + + return member.getTextRangeInParent(); + } + + /** 获取对象方法的引用 */ + protected PsiMethod @NotNull [] getObjectMethods() { + /* 对象成员,如 a.b.c: + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + */ + ExpressionNode source = (ExpressionNode) getFirstChild(); + ObjectMemberNode method = (ObjectMemberNode) getLastChild(); + + PsiClass sourceClass = source.getResultType(); + if (sourceClass == null) { + return PsiMethod.EMPTY_ARRAY; + } + + String methodName = method.getText(); + return PsiClassImplUtil.findMethodsByName(sourceClass, methodName, true); + } + + /** 获取调用参数类型列表 */ + protected PsiClass[] getCalleeArgumentTypes() { + CalleeArgumentsNode node = (CalleeArgumentsNode) getLastChild(); + /* + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(',')(',') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('2') + PsiElement(')')(')') + */ + Collection argNodeList = PsiTreeUtil.findChildrenOfType(node, ExpressionNode.class); + + return argNodeList.stream().map(ExpressionNode::getResultType).toArray(PsiClass[]::new); + } + + /** + * 获取表达式结果的类型 + *

+ * null 为有效值,可能是字面量即为 null + */ + protected PsiClass getResultType() { + PsiElement first = getFirstChild(); + if (first instanceof LiteralNode literal) { + return literal.getDataType(); + } // + else if (first instanceof IdentifierNode identifier) { + return identifier.getDataType(); + } // + else if (isObjectMethodCall()) { + PsiMethod method = getCalleeMethod(); + PsiType returnType = method != null ? method.getReturnType() : null; + + if (returnType != null) { + String typeName = returnType.getCanonicalText(false); + return PsiClassHelper.findClass(getProject(), typeName); + } + return null; + } + + /* TODO 复杂运算,如 a + b + RuleSpecNode(expression_single) + RuleSpecNode(expression_single) + RuleSpecNode(identifier) + PsiElement(Identifier)('a') + PsiElement('+')('+') + RuleSpecNode(expression_single) + RuleSpecNode(identifier) + PsiElement(Identifier)('b') + */ + return null; + } + + protected boolean matchMethodArgs(PsiMethod method, PsiClass[] args) { + // TODO 依次比较方法的参数类型 + return method.getParameterList().getParametersCount() == args.length; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java index 2672a0e39..a3ec26ac9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java @@ -4,6 +4,8 @@ import com.intellij.lang.ASTNode; import org.jetbrains.annotations.NotNull; /** + * 函数声明节点 + * * @author flytreeleft * @date 2025-06-30 */ diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java new file mode 100644 index 000000000..70db7f8e2 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java @@ -0,0 +1,20 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * 标识符 + *

+ * 一般为变量名字 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class Identifier extends LeafPsiElement { + + public Identifier(@NotNull IElementType type, @NotNull CharSequence text) { + super(type, text); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java index d418a58c3..e89f65c89 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java @@ -1,16 +1,24 @@ package io.nop.idea.plugin.lang.script.psi; -import com.intellij.psi.impl.source.tree.LeafPsiElement; -import com.intellij.psi.tree.IElementType; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; import org.jetbrains.annotations.NotNull; /** + * {@link Identifier} 节点 + * * @author flytreeleft - * @date 2025-06-30 + * @date 2025-07-02 */ -public class IdentifierNode extends LeafPsiElement { +public class IdentifierNode extends RuleSpecNode { - public IdentifierNode(@NotNull IElementType type, @NotNull CharSequence text) { - super(type, text); + public IdentifierNode(@NotNull ASTNode node) { + super(node); + } + + /** 获取变量的数据类型 */ + public PsiClass getDataType() { + // TODO 至下而上查找上下文中的变量类型信息 + return null; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java index 22ec9ef15..5ce90932f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -13,6 +13,12 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-29 */ public class ImportSourceNode extends RuleSpecNode { + private static final JavaClassReferenceProvider provider = new JavaClassReferenceProvider(); + + static { + // 支持解析包名:JavaClassReference#advancedResolveInner + provider.setOption(JavaClassReferenceProvider.ADVANCED_RESOLVE, true); + } public ImportSourceNode(@NotNull ASTNode node) { super(node); @@ -20,13 +26,9 @@ public class ImportSourceNode extends RuleSpecNode { /** 构造 Java 相关的引用对象,从而支持自动补全、引用跳转、文档显示等 */ @Override - public PsiReference @NotNull [] getReferences() { + protected PsiReference @NotNull [] doGetReferences() { String fqn = getText(); - JavaClassReferenceProvider provider = new JavaClassReferenceProvider(); - // 支持解析包名:JavaClassReference#advancedResolveInner - provider.setOption(JavaClassReferenceProvider.ADVANCED_RESOLVE, true); - JavaClassReferenceSet refSet = new JavaClassReferenceSet(fqn, this, 0, false, provider); return refSet.getReferences(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java new file mode 100644 index 000000000..d1137951f --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java @@ -0,0 +1,48 @@ +package io.nop.idea.plugin.lang.script.psi; + +import java.util.regex.Pattern; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import io.nop.idea.plugin.utils.PsiClassHelper; +import io.nop.xlang.parse.antlr.XLangLexer; +import org.antlr.intellij.adaptor.lexer.TokenIElementType; +import org.jetbrains.annotations.NotNull; + +/** + * 字面量节点 + * + * @author flytreeleft + * @date 2025-07-02 + */ +public class LiteralNode extends RuleSpecNode { + + public LiteralNode(@NotNull ASTNode node) { + super(node); + } + + /** 获取字面量的数据类型 */ + public PsiClass getDataType() { + LeafPsiElement target = (LeafPsiElement) getFirstChild().getFirstChild(); + + String typeName = switch (((TokenIElementType) target.getElementType()).getANTLRTokenType()) { + case XLangLexer.NullLiteral // + -> null; + case XLangLexer.BooleanLiteral // + -> Boolean.class.getName(); + case XLangLexer.DecimalLiteral // + -> Float.class.getName(); + case XLangLexer.BinaryIntegerLiteral, XLangLexer.DecimalIntegerLiteral, // + XLangLexer.HexIntegerLiteral // + -> Integer.class.getName(); + case XLangLexer.StringLiteral, XLangLexer.TemplateStringLiteral // + -> String.class.getName(); + case XLangLexer.RegularExpressionLiteral // + -> Pattern.class.getName(); + default -> null; + }; + + return typeName != null ? PsiClassHelper.findClass(getProject(), typeName) : null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionElementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java similarity index 36% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionElementNode.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java index 3d9bfcc60..27f671f41 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionElementNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java @@ -4,26 +4,17 @@ import com.intellij.lang.ASTNode; import org.jetbrains.annotations.NotNull; /** - * 表达式中的元素节点 - *

- * 其叶子节点可以为引用的变量名(identifier 类型),也可以为字面量(literal 类型)。 - * 其可以多层嵌套: + * 对象声明节点,如: *

- * 表达式 a.b.c(1, 2, 3) 除自身外,还包含以下元素
- * - a
- * - a.b
- * - a.b.c
- * - 1
- * - 2
- * - 3
+ * {a, b: 1}
  * 
* * @author flytreeleft - * @date 2025-06-30 + * @date 2025-07-02 */ -public class ExpressionElementNode extends RuleSpecNode { +public class ObjectDeclarationNode extends RuleSpecNode { - public ExpressionElementNode(@NotNull ASTNode node) { + public ObjectDeclarationNode(@NotNull ASTNode node) { super(node); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectMemberNode.java similarity index 70% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyNode.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectMemberNode.java index 99a55daf2..8721ec35d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectMemberNode.java @@ -4,7 +4,7 @@ import com.intellij.lang.ASTNode; import org.jetbrains.annotations.NotNull; /** - * 对象的属性节点 + * 对象成员(包含成员变量和方法)节点 *

* 如 a.b.c(){b, c: 1} 中, * bc 均为该类型节点 @@ -12,9 +12,9 @@ import org.jetbrains.annotations.NotNull; * @author flytreeleft * @date 2025-06-30 */ -public class ObjectPropertyNode extends RuleSpecNode { +public class ObjectMemberNode extends RuleSpecNode { - public ObjectPropertyNode(@NotNull ASTNode node) { + public ObjectMemberNode(@NotNull ASTNode node) { super(node); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java new file mode 100644 index 000000000..1e38dcbae --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java @@ -0,0 +1,20 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * 对象属性声明节点 + *

+ * 如 {a, b: 1} 中的 + * ab: 1 均为该类型节点 + * + * @author flytreeleft + * @date 2025-07-02 + */ +public class ObjectPropertyDeclarationNode extends RuleSpecNode { + + public ObjectPropertyDeclarationNode(@NotNull ASTNode node) { + super(node); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java index 9202d7cd6..bf1c0319b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java @@ -2,7 +2,9 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.extapi.psi.ASTWrapperPsiElement; import com.intellij.lang.ASTNode; +import com.intellij.openapi.application.ReadAction; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; import org.antlr.intellij.adaptor.psi.Trees; import org.jetbrains.annotations.NotNull; @@ -17,5 +19,17 @@ public class RuleSpecNode extends ASTWrapperPsiElement { } @Override - public PsiElement @NotNull [] getChildren() {return Trees.getChildren(this);} + public PsiElement @NotNull [] getChildren() { + return Trees.getChildren(this); + } + + @Override + public PsiReference @NotNull [] getReferences() { + // 在没有写入动作时,才执行函数并返回结果,从而避免阻塞编辑操作 + return ReadAction.compute(this::doGetReferences); + } + + protected PsiReference @NotNull [] doGetReferences() { + return PsiReference.EMPTY_ARRAY; + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java new file mode 100644 index 000000000..64616a0d7 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java @@ -0,0 +1,33 @@ +package io.nop.idea.plugin.lang.script.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiReferenceBase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对类方法的引用 + * + * @author flytreeleft + * @date 2025-07-01 + */ +public class ClassMethodReference extends PsiReferenceBase { + private final PsiMethod method; + + public ClassMethodReference(@NotNull PsiElement element, PsiMethod method, TextRange rangeInElement) { + super(element, rangeInElement); + this.method = method; + } + + @Override + public @Nullable PsiElement resolve() { + return this.method; + } + + @Override + public Object @NotNull [] getVariants() { + return super.getVariants(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java new file mode 100644 index 000000000..48cbbf9bc --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java @@ -0,0 +1,30 @@ +package io.nop.idea.plugin.lang.script.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReferenceBase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对类属性的引用 + * + * @author flytreeleft + * @date 2025-07-01 + */ +public class ClassPropertyReference extends PsiReferenceBase { + + public ClassPropertyReference(@NotNull PsiElement element, TextRange rangeInElement) { + super(element, rangeInElement); + } + + @Override + public @Nullable PsiElement resolve() { + return null; + } + + @Override + public Object @NotNull [] getVariants() { + return super.getVariants(); + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index e3014539a..b8c41f46e 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -15,45 +15,60 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); public void testParseStatement() { -// checkMatchJavaParseTree("import java.lang.;", "/test/ast/statement-err-1.ast"); -// checkMatchJavaParseTree("const abc = ", "/test/ast/statement-err-2.ast"); -// checkMatchJavaParseTree("const abc = () =>", "/test/ast/statement-err-3.ast"); +// assertParseTree("import java.lang.;", "/test/ast/statement-err-1.ast"); +// assertParseTree("const abc = ", "/test/ast/statement-err-2.ast"); +// assertParseTree("const abc = () =>", "/test/ast/statement-err-3.ast"); // -// checkMatchJavaParseTree("import java.lang.String;", "/test/ast/statement-1.ast"); +// assertParseTree("import java.lang.String;", "/test/ast/statement-1.ast"); // -// checkMatchJavaParseTree(""" -// import java.lang.String; -// import java.lang.Number; -// """, "/test/ast/statement-2.ast"); +// assertParseTree(""" +// import java.lang.String; +// import java.lang.Number; +// """, "/test/ast/statement-2.ast"); // -// checkMatchJavaParseTree(""" -// import java.lang.String; -// import java.lang.Number; -// const abc = new String("abc"); -// const def = 123; -// """, "/test/ast/statement-3.ast"); +// assertParseTree(""" +// import java.lang.String; +// import java.lang.Number; +// const abc = new String("abc"); +// const def = 123; +// """, "/test/ast/statement-3.ast"); // -// checkMatchJavaParseTree(""" -// import java.lang.Number; -// const fn1 = (a, b) => a + b; -// function fn2(a, b) { -// return a + b; -// } -// function fn3(a: string, b: number) { -// return a + b; -// } -// """, "/test/ast/statement-4.ast"); +// assertParseTree(""" +// import java.lang.Number; +// const fn1 = (a, b) => a + b; +// function fn2(a, b) { +// return a + b; +// } +// function fn3(a: string, b: number) { +// return a + b; +// } +// """, "/test/ast/statement-4.ast"); - checkMatchJavaParseTree(""" - const abc = ormTemplate.findListByQuery(query, mapper); - query.addFilter(filter(query, svcCtx)); - a.b.c(1, 2, 3); - const def = {a, b: 1}; - """, // - "/test/ast/statement-5.ast"); + assertParseTree(""" + const abc = ormTemplate.findListByQuery(query, mapper); + query.addFilter(filter(query, svcCtx)); + a(1, 2); + a.b.c(1, 2); + const c = a.b.c; + const def = {a, b: 1}; + """, // + "/test/ast/statement-5.ast"); } - protected void checkMatchJavaParseTree(String code, String expectedAstFile) { + public void testJavaParseTree() { + assertJavaParseTree(""" + public class Sample { + public static void main(String[] args) { + String s1 = StringHelper.trim(b); + String s2 = a.b.c(1, 2); + String s3 = a.b.c.e; + some.other.another.start(); + } + } + """); + } + + protected void assertParseTree(String code, String expectedAstFile) { PsiFile testFile = myFixture.configureByText("sample." + ext, code); String testTree = toParseTreeText(testFile); @@ -62,7 +77,14 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { assertEquals(expectedTree, testTree); } + protected void assertJavaParseTree(String code) { + PsiFile testFile = myFixture.configureByText("Sample.java", code); + String testTree = toParseTreeText(testFile); + + assertEquals("", testTree); + } + protected String toParseTreeText(@NotNull PsiElement file) { - return DebugUtil.psiToString(file, false, true); + return DebugUtil.psiToString(file, false, false); } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java index 9a73a6dae..7ee97295d 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -7,7 +7,9 @@ */ package io.nop.idea.plugin.lang; +import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiPackage; import com.intellij.psi.PsiReference; import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.idea.plugin.lang.script.XLangScriptFileType; @@ -19,25 +21,63 @@ import io.nop.idea.plugin.lang.script.XLangScriptFileType; public class TestXLangScriptReferences extends BaseXLangPluginTestCase { private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); + /** 测试对导入包/类的引用 */ public void testImportReference() { // Note: 需确保语法完整 // 导入包:只能得到光标之前的已存在包 - doTest("import io.nop.xlang.xdef;", "io.nop.xlang"); + assertReference("import io.nop.xlang.xdef;", "io.nop.xlang.xdef"); // 导入类 - doTest("import io.nop.xlang.xdef.XDefOverride;", "io.nop.xlang.xdef"); - doTest("import io.nop.xlang.xdef.XDefOverride;", "io.nop.xlang.xdef.XDefOverride"); + assertReference("import io.nop.xlang.xdef.XDefOverride;", "io.nop.xlang.xdef"); + assertReference("import io.nop.xlang.xdef.XDefOverride;", "io.nop.xlang.xdef.XDefOverride"); + } + + /** 测试对对象成员的引用 */ + public void testObjectMemberReference() { + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const handler = new XJsonDomainHandler(); + handler.getName(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName()"); + } + + public void testJavaClassMemberReference() { + assertJavaReference(""" + public class Sample { + public static void main(String[] args) { + String a = " abc ".trim(); + } + } + """, ""); } /** 通过在 text 中插入 <caret> 代表光标位置 */ - private void doTest(String text, String expected) { - myFixture.configureByText("sample." + ext, text); + private void assertReference(String text, String expected) { + assertReference("sample." + ext, text, expected); + } + + private void assertJavaReference(String text, String expected) { + assertReference("Sample.java", text, expected); + } + + private void assertReference(String fileName, String text, String expected) { + myFixture.configureByText(fileName, text); PsiReference ref = findReferenceAtCaret(); assertNotNull(ref); PsiElement target = ref.resolve(); assertNotNull(target); + + if (target instanceof PsiClass cls) { + String actual = cls.getQualifiedName(); + assertEquals(expected, actual); + } else if (target instanceof PsiPackage pkg) { + String actual = pkg.getQualifiedName(); + assertEquals(expected, actual); + } else { + fail("Unknown target " + target); + } } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast index cc1adfd29..993a6c875 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast @@ -1,145 +1,190 @@ -FILE(0,135) - ProgramNode(program)(0,134) - RuleSpecNode(topLevelStatements_)(0,134) - StatementRootNode(ast_topLevelStatement)(0,55) - RuleSpecNode(statement)(0,55) - VariableDeclarationNode(variableDeclaration)(0,55) - RuleSpecNode(varModifier_)(0,5) - PsiElement('const')('const')(0,5) - RuleSpecNode(variableDeclarators_)(6,54) - RuleSpecNode(variableDeclarator)(6,54) - RuleSpecNode(ast_identifierOrPattern)(6,9) - RuleSpecNode(identifier)(6,9) - PsiElement(Identifier)('abc')(6,9) - RuleSpecNode(expression_initializer)(10,54) - PsiElement('=')('=')(10,11) - RuleSpecNode(expression_single)(12,54) - RuleSpecNode(expression_single)(12,39) - RuleSpecNode(expression_single)(12,23) - RuleSpecNode(identifier)(12,23) - PsiElement(Identifier)('ormTemplate')(12,23) - PsiElement('.')('.')(23,24) - ObjectPropertyNode(identifier_ex)(24,39) - RuleSpecNode(identifierOrKeyword_)(24,39) - RuleSpecNode(identifier)(24,39) - PsiElement(Identifier)('findListByQuery')(24,39) - CalleeArgumentsNode(arguments_)(39,54) - PsiElement('(')('(')(39,40) - RuleSpecNode(expression_single)(40,45) - RuleSpecNode(identifier)(40,45) - PsiElement(Identifier)('query')(40,45) - PsiElement(',')(',')(45,46) - RuleSpecNode(expression_single)(47,53) - RuleSpecNode(identifier)(47,53) - PsiElement(Identifier)('mapper')(47,53) - PsiElement(')')(')')(53,54) - RuleSpecNode(eos__)(54,55) - PsiElement(';')(';')(54,55) - StatementRootNode(ast_topLevelStatement)(56,95) - RuleSpecNode(statement)(56,95) - RuleSpecNode(expressionStatement)(56,95) - RuleSpecNode(expression_single)(56,94) - RuleSpecNode(expression_single)(56,71) - RuleSpecNode(expression_single)(56,61) - RuleSpecNode(identifier)(56,61) - PsiElement(Identifier)('query')(56,61) - PsiElement('.')('.')(61,62) - ObjectPropertyNode(identifier_ex)(62,71) - RuleSpecNode(identifierOrKeyword_)(62,71) - RuleSpecNode(identifier)(62,71) - PsiElement(Identifier)('addFilter')(62,71) - CalleeArgumentsNode(arguments_)(71,94) - PsiElement('(')('(')(71,72) - RuleSpecNode(expression_single)(72,93) - RuleSpecNode(expression_single)(72,78) - RuleSpecNode(identifier)(72,78) - PsiElement(Identifier)('filter')(72,78) - CalleeArgumentsNode(arguments_)(78,93) - PsiElement('(')('(')(78,79) - RuleSpecNode(expression_single)(79,84) - RuleSpecNode(identifier)(79,84) - PsiElement(Identifier)('query')(79,84) - PsiElement(',')(',')(84,85) - RuleSpecNode(expression_single)(86,92) - RuleSpecNode(identifier)(86,92) - PsiElement(Identifier)('svcCtx')(86,92) - PsiElement(')')(')')(92,93) - PsiElement(')')(')')(93,94) - RuleSpecNode(eos__)(94,95) - PsiElement(';')(';')(94,95) - StatementRootNode(ast_topLevelStatement)(96,111) - RuleSpecNode(statement)(96,111) - RuleSpecNode(expressionStatement)(96,111) - RuleSpecNode(expression_single)(96,110) - RuleSpecNode(expression_single)(96,101) - RuleSpecNode(expression_single)(96,99) - RuleSpecNode(expression_single)(96,97) - RuleSpecNode(identifier)(96,97) - PsiElement(Identifier)('a')(96,97) - PsiElement('.')('.')(97,98) - ObjectPropertyNode(identifier_ex)(98,99) - RuleSpecNode(identifierOrKeyword_)(98,99) - RuleSpecNode(identifier)(98,99) - PsiElement(Identifier)('b')(98,99) - PsiElement('.')('.')(99,100) - ObjectPropertyNode(identifier_ex)(100,101) - RuleSpecNode(identifierOrKeyword_)(100,101) - RuleSpecNode(identifier)(100,101) - PsiElement(Identifier)('c')(100,101) - CalleeArgumentsNode(arguments_)(101,110) - PsiElement('(')('(')(101,102) - RuleSpecNode(expression_single)(102,103) - RuleSpecNode(literal)(102,103) - RuleSpecNode(literal_numeric)(102,103) - PsiElement(DecimalIntegerLiteral)('1')(102,103) - PsiElement(',')(',')(103,104) - RuleSpecNode(expression_single)(105,106) - RuleSpecNode(literal)(105,106) - RuleSpecNode(literal_numeric)(105,106) - PsiElement(DecimalIntegerLiteral)('2')(105,106) - PsiElement(',')(',')(106,107) - RuleSpecNode(expression_single)(108,109) - RuleSpecNode(literal)(108,109) - RuleSpecNode(literal_numeric)(108,109) - PsiElement(DecimalIntegerLiteral)('3')(108,109) - PsiElement(')')(')')(109,110) - RuleSpecNode(eos__)(110,111) - PsiElement(';')(';')(110,111) - StatementRootNode(ast_topLevelStatement)(112,134) - RuleSpecNode(statement)(112,134) - VariableDeclarationNode(variableDeclaration)(112,134) - RuleSpecNode(varModifier_)(112,117) - PsiElement('const')('const')(112,117) - RuleSpecNode(variableDeclarators_)(118,133) - RuleSpecNode(variableDeclarator)(118,133) - RuleSpecNode(ast_identifierOrPattern)(118,121) - RuleSpecNode(identifier)(118,121) - PsiElement(Identifier)('def')(118,121) - RuleSpecNode(expression_initializer)(122,133) - PsiElement('=')('=')(122,123) - RuleSpecNode(expression_single)(124,133) - RuleSpecNode(objectExpression)(124,133) - PsiElement('{')('{')(124,125) - RuleSpecNode(objectProperties_)(125,132) - RuleSpecNode(ast_objectProperty)(125,126) - RuleSpecNode(propertyAssignment)(125,126) - ObjectPropertyNode(identifier_ex)(125,126) - RuleSpecNode(identifierOrKeyword_)(125,126) - RuleSpecNode(identifier)(125,126) - PsiElement(Identifier)('a')(125,126) - PsiElement(',')(',')(126,127) - RuleSpecNode(ast_objectProperty)(128,132) - RuleSpecNode(propertyAssignment)(128,132) - RuleSpecNode(expression_propName)(128,129) - ObjectPropertyNode(identifier_ex)(128,129) - RuleSpecNode(identifierOrKeyword_)(128,129) - RuleSpecNode(identifier)(128,129) - PsiElement(Identifier)('b')(128,129) - PsiElement(':')(':')(129,130) - RuleSpecNode(expression_single)(131,132) - RuleSpecNode(literal)(131,132) - RuleSpecNode(literal_numeric)(131,132) - PsiElement(DecimalIntegerLiteral)('1')(131,132) - PsiElement('}')('}')(132,133) - RuleSpecNode(eos__)(133,134) - PsiElement(';')(';')(133,134) +FILE + ProgramNode(program) + RuleSpecNode(topLevelStatements_) + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('ormTemplate') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('findListByQuery') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('query') + PsiElement(',')(',') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('mapper') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + RuleSpecNode(expressionStatement) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('query') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('addFilter') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('filter') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('query') + PsiElement(',')(',') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('svcCtx') + PsiElement(')')(')') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + RuleSpecNode(expressionStatement) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(',')(',') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('2') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + RuleSpecNode(expressionStatement) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(',')(',') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('2') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('def') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + ObjectDeclarationNode(objectExpression) + PsiElement('{')('{') + RuleSpecNode(objectProperties_) + ObjectPropertyDeclarationNode(ast_objectProperty) + ObjectPropertyAssignmentNode(propertyAssignment) + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement(',')(',') + ObjectPropertyDeclarationNode(ast_objectProperty) + ObjectPropertyAssignmentNode(propertyAssignment) + RuleSpecNode(expression_propName) + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement(':')(':') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement('}')('}') + RuleSpecNode(eos__) + PsiElement(';')(';') -- Gitee From 8d32ba745d5b27dcb42d09c1cbda32c176b875f1 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 3 Jul 2025 13:04:30 +0800 Subject: [PATCH 31/82] =?UTF-8?q?nop-idea-pugin:=20=E5=88=9D=E6=AD=A5?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=AF=B9=20XLang=20Script=20=E4=B8=AD?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E6=96=B9=E6=B3=95=E5=92=8C=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=E6=9E=84=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../script/XLangScriptParserDefinition.java | 10 +- .../lang/script/psi/ArrowFunctionNode.java | 7 + .../lang/script/psi/ExpressionNode.java | 269 ++++++--- .../plugin/lang/script/psi/Identifier.java | 2 +- .../lang/script/psi/IdentifierNode.java | 10 +- .../script/psi/ImportDeclarationNode.java | 37 ++ .../lang/script/psi/ImportSourceNode.java | 9 + .../plugin/lang/script/psi/ProgramNode.java | 2 - ...RootNode.java => ReturnStatementNode.java} | 10 +- .../plugin/lang/script/psi/RuleSpecNode.java | 43 ++ .../plugin/lang/script/psi/StatementNode.java | 57 ++ .../script/psi/TopLevelStatementNode.java | 36 ++ .../reference/ClassMethodReference.java | 2 +- .../reference/ClassPropertyReference.java | 7 +- .../plugin/lang/TestXLangScriptParser.java | 48 +- .../resources/_vfs/test/ast/statement-1.ast | 510 +++++++++++++++++- .../resources/_vfs/test/ast/statement-2.ast | 45 -- .../resources/_vfs/test/ast/statement-3.ast | 92 ---- .../resources/_vfs/test/ast/statement-4.ast | 142 ----- .../resources/_vfs/test/ast/statement-5.ast | 190 ------- 20 files changed, 912 insertions(+), 616 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/{StatementRootNode.java => ReturnStatementNode.java} (49%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java delete mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-2.ast delete mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-3.ast delete mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-4.ast delete mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index 91885cdce..65742c238 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -29,8 +29,10 @@ import io.nop.idea.plugin.lang.script.psi.ObjectPropertyAssignmentNode; import io.nop.idea.plugin.lang.script.psi.ObjectPropertyDeclarationNode; import io.nop.idea.plugin.lang.script.psi.ParameterizedTypeNode; import io.nop.idea.plugin.lang.script.psi.ProgramNode; +import io.nop.idea.plugin.lang.script.psi.ReturnStatementNode; import io.nop.idea.plugin.lang.script.psi.RuleSpecNode; -import io.nop.idea.plugin.lang.script.psi.StatementRootNode; +import io.nop.idea.plugin.lang.script.psi.StatementNode; +import io.nop.idea.plugin.lang.script.psi.TopLevelStatementNode; import io.nop.idea.plugin.lang.script.psi.VariableDeclarationNode; import io.nop.xlang.parse.antlr.XLangLexer; import io.nop.xlang.parse.antlr.XLangParser; @@ -96,7 +98,9 @@ public class XLangScriptParserDefinition implements ParserDefinition { case XLangParser.RULE_program -> // new ProgramNode(node); case XLangParser.RULE_ast_topLevelStatement -> // - new StatementRootNode(node); + new TopLevelStatementNode(node); + case XLangParser.RULE_statement -> // + new StatementNode(node); case XLangParser.RULE_identifier -> // new IdentifierNode(node); case XLangParser.RULE_literal -> // @@ -135,6 +139,8 @@ public class XLangScriptParserDefinition implements ParserDefinition { new ArrowFunctionNode(node); case XLangParser.RULE_expression_functionBody -> // new ArrowFunctionBodyNode(node); + case XLangParser.RULE_returnStatement -> // + new ReturnStatementNode(node); default -> new RuleSpecNode(node); }; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java index 14b6c9970..f487f608b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java @@ -1,6 +1,7 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; import org.jetbrains.annotations.NotNull; /** @@ -14,4 +15,10 @@ public class ArrowFunctionNode extends RuleSpecNode { public ArrowFunctionNode(@NotNull ASTNode node) { super(node); } + + /** 获取函数的返回值类型 */ + public PsiClass getReturnType() { + // TODO 分析 return 表达式,得到返回类型 + return null; + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index 156874b84..4655e8397 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -1,16 +1,20 @@ package io.nop.idea.plugin.lang.script.psi; import java.util.Collection; +import java.util.function.BiFunction; import com.intellij.lang.ASTNode; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiType; import com.intellij.psi.impl.PsiClassImplUtil; import com.intellij.psi.util.PsiTreeUtil; +import io.nop.idea.plugin.lang.script.reference.ClassMethodReference; +import io.nop.idea.plugin.lang.script.reference.ClassPropertyReference; import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; @@ -42,64 +46,124 @@ public class ExpressionNode extends RuleSpecNode { super(node); } - /** 是否为标识符 */ + @Override + public PsiReference @NotNull [] doGetReferences() { + // 对象声明:{a, b: 1} + if (isObjectDeclaration()) { + return PsiReference.EMPTY_ARRAY; + } + // 对象方法调用:a.b.c(1, 2) + else if (isObjectMethodCall()) { + ExpressionNode obj = (ExpressionNode) getFirstChild(); + PsiMethod method = getObjectMethod(); + + if (method != null) { + // Note: 需加上相对于当前表达式的对象偏移量 + TextRange methodTextRange = obj.getObjectMemberTextRange().shiftLeft(obj.getStartOffsetInParent()); + ClassMethodReference ref = new ClassMethodReference(this, method, methodTextRange); + + return new PsiReference[] { ref }; + } + } + // 函数调用:fn1(1, 2, 3) + else if (isFunctionCall()) { + // TODO 构造函数引用 + return PsiReference.EMPTY_ARRAY; + } + // 对象属性访问:a.b.c + else if (isObjectMember()) { + PsiField prop = getObjectProperty(); + if (prop != null) { + TextRange propTextRange = getObjectMemberTextRange(); + ClassPropertyReference ref = new ClassPropertyReference(this, prop, propTextRange); + + return new PsiReference[] { ref }; + } + } + + return PsiReference.EMPTY_ARRAY; + } + + /** 当前表达式是否为标识符 */ public boolean isIdentifier() { return getFirstChild() instanceof IdentifierNode; } - /** 是否为字面量 */ + /** 当前表达式是否为字面量 */ public boolean isLiteral() { return getFirstChild() instanceof LiteralNode; } - /** 是否为对象成员(成员变量或方法)表达式 */ + /** 当前表达式是否为访问对象成员(成员变量或方法) */ public boolean isObjectMember() { - return getFirstChild() instanceof ObjectMemberNode; + /* 对象成员访问,如 a.b: + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + */ + if (getFirstChild() instanceof ExpressionNode obj) { + return getLastChild() instanceof ObjectMemberNode; + } + return false; } /** - * 是否为对象声明表达式 + * 当前表达式是否为对象声明 *

* {@link ObjectDeclarationNode} 的直接父节点为 {@link ExpressionNode}, * 因此,在构造 {@link ObjectMemberNode} 的成员引用时, * 一般不为对象声明中的属性构造引用 */ public boolean isObjectDeclaration() { + /* 对象声明,如 {a, b: 1}: + ExpressionNode(expression_single) + ObjectDeclarationNode(objectExpression) + PsiElement('{')('{') + RuleSpecNode(objectProperties_) + ObjectPropertyDeclarationNode(ast_objectProperty) + ObjectPropertyAssignmentNode(propertyAssignment) + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement(',')(',') + ObjectPropertyDeclarationNode(ast_objectProperty) + ObjectPropertyAssignmentNode(propertyAssignment) + RuleSpecNode(expression_propName) + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement(':')(':') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement('}')('}') + */ return getFirstChild() instanceof ObjectDeclarationNode; } - /** 是否为对象方法调用表达式 */ + /** 当前表达式是否为对象方法调用 */ public boolean isObjectMethodCall() { - return getLastChild() instanceof CalleeArgumentsNode; - } - - @Override - public PsiReference @NotNull [] doGetReferences() { - if (isObjectDeclaration()) { - return PsiReference.EMPTY_ARRAY; - } // - else if (isObjectMethodCall()) { -// // Note: 需加上相对于当前表达式的对象偏移量 -// TextRange calleeMethodTextRange = callee.getObjectMemberTextRange().shiftLeft(callee.getStartOffsetInParent()); - return PsiReference.EMPTY_ARRAY; - } - - return PsiReference.EMPTY_ARRAY; - } - - /** - * 获取调用方的方法 - *

- * 注意,只有通过参数列表才能唯一确定调用方的方法 - */ - protected PsiMethod getCalleeMethod() { - ExpressionNode callee = (ExpressionNode) getFirstChild(); - - /* 函数调用,如 a(1): + /* 对象方法调用,如 a.b(1): ExpressionNode(expression_single) ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') CalleeArgumentsNode(arguments_) PsiElement('(')('(') ExpressionNode(expression_single) @@ -108,21 +172,41 @@ public class ExpressionNode extends RuleSpecNode { PsiElement(DecimalIntegerLiteral)('1') PsiElement(')')(')') */ - if (callee.isIdentifier()) { - return null; + if (getFirstChild() instanceof ExpressionNode obj) { + return obj.isObjectMember() && getLastChild() instanceof CalleeArgumentsNode; } + return false; + } - /* 对象方法调用,如 a.b(1): + /** 当前表达式是否为对象构造函数调用 */ + public boolean isObjectConstructorCall() { + /* 对象构造方法调用,如 new String("abc"): ExpressionNode(expression_single) - ExpressionNode(expression_single) + PsiElement('new')('new') + ParameterizedTypeNode(parameterizedTypeNode) + RuleSpecNode(qualifiedName_) + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('String') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('b') + LiteralNode(literal) + RuleSpecNode(literal_string) + PsiElement(StringLiteral)('"abc"') + PsiElement(')')(')') + */ + return getFirstChild().getText().equals("new") && getLastChild() instanceof CalleeArgumentsNode; + } + + /** 当前表达式是否为函数调用 */ + public boolean isFunctionCall() { + /* 函数调用,如 a(1): + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') CalleeArgumentsNode(arguments_) PsiElement('(')('(') ExpressionNode(expression_single) @@ -131,15 +215,27 @@ public class ExpressionNode extends RuleSpecNode { PsiElement(DecimalIntegerLiteral)('1') PsiElement(')')(')') */ - if (!callee.isObjectMember()) { - return null; + if (getFirstChild() instanceof ExpressionNode callee) { + return callee.isIdentifier() && getLastChild() instanceof CalleeArgumentsNode; } + return false; + } + + /** 当前表达式是否为箭头函数 */ + public boolean isArrowFunction() { + return getFirstChild() instanceof ArrowFunctionNode; + } + + /** 获取对象的方法 */ + protected PsiMethod getObjectMethod() { + ExpressionNode obj = (ExpressionNode) getFirstChild(); - PsiMethod[] calleeMethods = callee.getObjectMethods(); - PsiClass[] calleeMethodArgTypes = getCalleeArgumentTypes(); + PsiMethod[] objMethods = obj.getObjectMethods(); + PsiClass[] objMethodArgTypes = getObjectMethodArgumentTypes(); - for (PsiMethod method : calleeMethods) { - if (matchMethodArgs(method, calleeMethodArgTypes)) { + // Note: 只有通过参数列表才能唯一确定调用方的方法 + for (PsiMethod method : objMethods) { + if (matchMethodArgs(method, objMethodArgTypes)) { return method; } } @@ -153,39 +249,33 @@ public class ExpressionNode extends RuleSpecNode { return member.getTextRangeInParent(); } - /** 获取对象方法的引用 */ + /** 获取对象的方法 */ protected PsiMethod @NotNull [] getObjectMethods() { - /* 对象成员,如 a.b.c: - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('b') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('c') - */ - ExpressionNode source = (ExpressionNode) getFirstChild(); - ObjectMemberNode method = (ObjectMemberNode) getLastChild(); + return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findMethodsByName(objClass, + memberName, + true)); + } + + /** 获取对象的属性 */ + protected PsiField getObjectProperty() { + return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findFieldByName(objClass, memberName, true)); + } - PsiClass sourceClass = source.getResultType(); - if (sourceClass == null) { - return PsiMethod.EMPTY_ARRAY; + protected T getObjectMember(BiFunction consumer) { + ExpressionNode obj = (ExpressionNode) getFirstChild(); + ObjectMemberNode member = (ObjectMemberNode) getLastChild(); + + PsiClass objClass = obj.getResultType(); + if (objClass == null) { + return null; } - String methodName = method.getText(); - return PsiClassImplUtil.findMethodsByName(sourceClass, methodName, true); + String memberName = member.getText(); + return consumer.apply(objClass, memberName); } /** 获取调用参数类型列表 */ - protected PsiClass[] getCalleeArgumentTypes() { + protected PsiClass[] getObjectMethodArgumentTypes() { CalleeArgumentsNode node = (CalleeArgumentsNode) getLastChild(); /* CalleeArgumentsNode(arguments_) @@ -212,22 +302,39 @@ public class ExpressionNode extends RuleSpecNode { * null 为有效值,可能是字面量即为 null */ protected PsiClass getResultType() { - PsiElement first = getFirstChild(); - if (first instanceof LiteralNode literal) { + PsiElement firstChild = getFirstChild(); + + if (firstChild instanceof LiteralNode literal) { return literal.getDataType(); } // - else if (first instanceof IdentifierNode identifier) { + else if (firstChild instanceof IdentifierNode identifier) { return identifier.getDataType(); } // else if (isObjectMethodCall()) { - PsiMethod method = getCalleeMethod(); + PsiMethod method = getObjectMethod(); PsiType returnType = method != null ? method.getReturnType() : null; if (returnType != null) { String typeName = returnType.getCanonicalText(false); + return PsiClassHelper.findClass(getProject(), typeName); } return null; + } // + else if (isObjectConstructorCall()) { + ParameterizedTypeNode cst = PsiTreeUtil.findChildOfType(this, ParameterizedTypeNode.class); + IdentifierNode typeNode = (IdentifierNode) PsiTreeUtil.getDeepestLast(cst).getParent(); + + return typeNode.getDataType(); + } // + else if (isFunctionCall()) { + // TODO 分析 return 表达式,得到返回类型 + return null; + } // + else if (isArrowFunction()) { + ArrowFunctionNode fn = (ArrowFunctionNode) getFirstChild(); + + return fn.getReturnType(); } /* TODO 复杂运算,如 a + b diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java index 70db7f8e2..fbd464711 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java @@ -7,7 +7,7 @@ import org.jetbrains.annotations.NotNull; /** * 标识符 *

- * 一般为变量名字 + * 一般为变量名字或导入的类名 * * @author flytreeleft * @date 2025-06-30 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java index e89f65c89..902ef8418 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java @@ -1,5 +1,7 @@ package io.nop.idea.plugin.lang.script.psi; +import java.util.Map; + import com.intellij.lang.ASTNode; import com.intellij.psi.PsiClass; import org.jetbrains.annotations.NotNull; @@ -18,7 +20,11 @@ public class IdentifierNode extends RuleSpecNode { /** 获取变量的数据类型 */ public PsiClass getDataType() { - // TODO 至下而上查找上下文中的变量类型信息 - return null; + String varName = getText(); + + Map vars = getVisibleVarTypes(); + VarDecl varDecl = vars.get(varName); + + return varDecl != null ? varDecl.type : null; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java index d0a938856..7b3b9c794 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java @@ -1,6 +1,11 @@ package io.nop.idea.plugin.lang.script.psi; +import java.util.Map; + import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import com.intellij.psi.util.PsiTreeUtil; +import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; /** @@ -14,4 +19,36 @@ public class ImportDeclarationNode extends RuleSpecNode { public ImportDeclarationNode(@NotNull ASTNode node) { super(node); } + + @Override + public @NotNull Map getVarTypes() { + /* 导入语句:import java.lang.Number; + ImportDeclarationNode(importAsDeclaration) + PsiElement('import')('import') + ImportSourceNode(ast_importSource) + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('java') + PsiElement('.')('.') + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('lang') + PsiElement('.')('.') + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('Number') + RuleSpecNode(eos__) + PsiElement(';')(';') + */ + ImportSourceNode imp = PsiTreeUtil.findChildOfType(this, ImportSourceNode.class); + + String clsName = imp.getClassName(); + String clsFqn = imp.getClassFullyQualifiedName(); + PsiClass cls = PsiClassHelper.findClass(getProject(), clsFqn); + + return Map.of(clsName, new VarDecl(cls, cls)); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java index 5ce90932f..8de9994a1 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -4,6 +4,7 @@ import com.intellij.lang.ASTNode; import com.intellij.psi.PsiReference; import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceProvider; import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceSet; +import com.intellij.psi.util.PsiTreeUtil; import org.jetbrains.annotations.NotNull; /** @@ -24,6 +25,14 @@ public class ImportSourceNode extends RuleSpecNode { super(node); } + public String getClassName() { + return PsiTreeUtil.getDeepestLast(this).getText(); + } + + public String getClassFullyQualifiedName() { + return getText(); + } + /** 构造 Java 相关的引用对象,从而支持自动补全、引用跳转、文档显示等 */ @Override protected PsiReference @NotNull [] doGetReferences() { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java index 257501e83..9fbaf8b21 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java @@ -17,6 +17,4 @@ public class ProgramNode extends RuleSpecNode { public ProgramNode(@NotNull ASTNode node) { super(node); } - - // TODO 获取上下文环境中可访问的变量 } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementRootNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java similarity index 49% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementRootNode.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java index 526252af9..c630a91c7 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementRootNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java @@ -4,16 +4,14 @@ import com.intellij.lang.ASTNode; import org.jetbrains.annotations.NotNull; /** - * 各种语句的根节点 - *

- * 赋值、函数、导入、try 等语句,各自均有唯一的根节点 + * return 语句节点 * * @author flytreeleft - * @date 2025-06-30 + * @date 2025-07-03 */ -public class StatementRootNode extends RuleSpecNode { +public class ReturnStatementNode extends RuleSpecNode { - public StatementRootNode(@NotNull ASTNode node) { + public ReturnStatementNode(@NotNull ASTNode node) { super(node); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java index bf1c0319b..1dcdfb068 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java @@ -1,8 +1,12 @@ package io.nop.idea.plugin.lang.script.psi; +import java.util.HashMap; +import java.util.Map; + import com.intellij.extapi.psi.ASTWrapperPsiElement; import com.intellij.lang.ASTNode; import com.intellij.openapi.application.ReadAction; +import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import org.antlr.intellij.adaptor.psi.Trees; @@ -29,7 +33,46 @@ public class RuleSpecNode extends ASTWrapperPsiElement { return ReadAction.compute(this::doGetReferences); } + /** 获取当前节点可访问到的变量及其类型 */ + public @NotNull Map getVisibleVarTypes() { + Map types = new HashMap<>(); + + PsiElement node = this; + while (node instanceof RuleSpecNode) { + PsiElement parent = node.getParent(); + + // Note: 下层的变量优先于上层的变量 + if (parent instanceof RuleSpecNode) { + for (PsiElement child : parent.getChildren()) { + if (child != node) { + ((RuleSpecNode) child).getVarTypes().forEach(types::putIfAbsent); + } + } + } else { + // TODO 从所在的 标签中获取 xlib 函数的参数列表以及内置变量列表 + } + + node = parent; + } + return types; + } + + /** 获取当前节点所定义的变量及其类型 */ + public @NotNull Map getVarTypes() { + return Map.of(); + } + protected PsiReference @NotNull [] doGetReferences() { return PsiReference.EMPTY_ARRAY; } + + public static class VarDecl { + public final PsiElement element; + public final PsiClass type; + + VarDecl(PsiElement element, PsiClass type) { + this.element = element; + this.type = type; + } + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java new file mode 100644 index 000000000..f2d399637 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java @@ -0,0 +1,57 @@ +package io.nop.idea.plugin.lang.script.psi; + +import java.util.Map; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-07-03 + */ +public class StatementNode extends RuleSpecNode { + + public StatementNode(@NotNull ASTNode node) { + super(node); + } + + @Override + public @NotNull Map getVarTypes() { + PsiElement firstChild = getFirstChild(); + + /* 变量声明:let def = 123; + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('def') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('123') + RuleSpecNode(eos__) + PsiElement(';')(';') + */ + if (firstChild instanceof VariableDeclarationNode declaration) { + RuleSpecNode declarator = (RuleSpecNode) declaration.getLastChild().getFirstChild(); + IdentifierNode identifier = (IdentifierNode) declarator.getFirstChild().getFirstChild(); + ExpressionNode expression = (ExpressionNode) declarator.getLastChild().getLastChild(); + + String varName = identifier.getText(); + PsiClass varType = expression.getResultType(); + VarDecl varDecl = new VarDecl(identifier, varType); + + return Map.of(varName, varDecl); + } + + return Map.of(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java new file mode 100644 index 000000000..5f750aaba --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java @@ -0,0 +1,36 @@ +package io.nop.idea.plugin.lang.script.psi; + +import java.util.Map; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; + +/** + * 各种语句的根节点 + *

+ * 赋值、函数、导入、try 等语句,各自均有唯一的根节点 + * + * @author flytreeleft + * @date 2025-06-30 + */ +public class TopLevelStatementNode extends RuleSpecNode { + + public TopLevelStatementNode(@NotNull ASTNode node) { + super(node); + } + + @Override + public @NotNull Map getVarTypes() { + PsiElement firstChild = getFirstChild(); + + if (firstChild instanceof StatementNode s) { + return s.getVarTypes(); + } // + else if (firstChild.getFirstChild() instanceof ImportDeclarationNode i) { + return i.getVarTypes(); + } + + return Map.of(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java index 64616a0d7..e50c7327c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java @@ -23,7 +23,7 @@ public class ClassMethodReference extends PsiReferenceBase { @Override public @Nullable PsiElement resolve() { - return this.method; + return method; } @Override diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java index 48cbbf9bc..254bd2e65 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java @@ -2,6 +2,7 @@ package io.nop.idea.plugin.lang.script.reference; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; import com.intellij.psi.PsiReferenceBase; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,14 +14,16 @@ import org.jetbrains.annotations.Nullable; * @date 2025-07-01 */ public class ClassPropertyReference extends PsiReferenceBase { + private final PsiField prop; - public ClassPropertyReference(@NotNull PsiElement element, TextRange rangeInElement) { + public ClassPropertyReference(@NotNull PsiElement element, PsiField prop, TextRange rangeInElement) { super(element, rangeInElement); + this.prop = prop; } @Override public @Nullable PsiElement resolve() { - return null; + return prop; } @Override diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index b8c41f46e..ffd16b5dc 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -19,40 +19,36 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { // assertParseTree("const abc = ", "/test/ast/statement-err-2.ast"); // assertParseTree("const abc = () =>", "/test/ast/statement-err-3.ast"); // -// assertParseTree("import java.lang.String;", "/test/ast/statement-1.ast"); -// -// assertParseTree(""" -// import java.lang.String; -// import java.lang.Number; -// """, "/test/ast/statement-2.ast"); -// -// assertParseTree(""" -// import java.lang.String; -// import java.lang.Number; -// const abc = new String("abc"); -// const def = 123; -// """, "/test/ast/statement-3.ast"); -// -// assertParseTree(""" -// import java.lang.Number; -// const fn1 = (a, b) => a + b; -// function fn2(a, b) { -// return a + b; -// } -// function fn3(a: string, b: number) { -// return a + b; -// } -// """, "/test/ast/statement-4.ast"); - assertParseTree(""" + import java.lang.String; + import java.lang.Number; + // const abc = ormTemplate.findListByQuery(query, mapper); query.addFilter(filter(query, svcCtx)); + // a(1, 2); a.b.c(1, 2); + // + let abc = new String("abc"); + let def = 123; const c = a.b.c; const def = {a, b: 1}; + // + const fn1 = (a, b) => a + b; + function fn2(a, b) { + const c = 5; + return a + b + c; + } + function fn3(a: string, b: number) { + return a + b; + } + // + if (a > 2) { + let b = 3; + a.b(b, 1); + } """, // - "/test/ast/statement-5.ast"); + "/test/ast/statement-1.ast"); } public void testJavaParseTree() { diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast index 18c93259f..b15d94f0a 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast @@ -1,24 +1,486 @@ -FILE(0,24) - ProgramNode(program)(0,24) - RuleSpecNode(topLevelStatements_)(0,24) - RuleSpecNode(ast_topLevelStatement)(0,24) - RuleSpecNode(moduleDeclaration_import)(0,24) - ImportAsDeclarationNode(importAsDeclaration)(0,24) - PsiElement('import')('import')(0,6) - ImportSourceNode(ast_importSource)(7,23) - ImportQualifiedNameNode(qualifiedName)(7,23) - RuleSpecNode(qualifiedName_name_)(7,11) - RuleSpecNode(identifier)(7,11) - PsiElement(Identifier)('java')(7,11) - PsiElement('.')('.')(11,12) - ImportQualifiedNameNode(qualifiedName)(12,23) - RuleSpecNode(qualifiedName_name_)(12,16) - RuleSpecNode(identifier)(12,16) - PsiElement(Identifier)('lang')(12,16) - PsiElement('.')('.')(16,17) - ImportQualifiedNameNode(qualifiedName)(17,23) - RuleSpecNode(qualifiedName_name_)(17,23) - RuleSpecNode(identifier)(17,23) - PsiElement(Identifier)('String')(17,23) - RuleSpecNode(eos__)(23,24) - PsiElement(';')(';')(23,24) +FILE + ProgramNode(program) + RuleSpecNode(topLevelStatements_) + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(moduleDeclaration_import) + ImportDeclarationNode(importAsDeclaration) + PsiElement('import')('import') + ImportSourceNode(ast_importSource) + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('java') + PsiElement('.')('.') + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('lang') + PsiElement('.')('.') + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('String') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(moduleDeclaration_import) + ImportDeclarationNode(importAsDeclaration) + PsiElement('import')('import') + ImportSourceNode(ast_importSource) + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('java') + PsiElement('.')('.') + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('lang') + PsiElement('.')('.') + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('Number') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiElement(SingleLineComment)('//') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('ormTemplate') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('findListByQuery') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('query') + PsiElement(',')(',') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('mapper') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + RuleSpecNode(expressionStatement) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('query') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('addFilter') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('filter') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('query') + PsiElement(',')(',') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('svcCtx') + PsiElement(')')(')') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiElement(SingleLineComment)('//') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + RuleSpecNode(expressionStatement) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(',')(',') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('2') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + RuleSpecNode(expressionStatement) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(',')(',') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('2') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiElement(SingleLineComment)('//') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + PsiElement('new')('new') + ParameterizedTypeNode(parameterizedTypeNode) + RuleSpecNode(qualifiedName_) + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('String') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_string) + PsiElement(StringLiteral)('"abc"') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('def') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('123') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('def') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + ObjectDeclarationNode(objectExpression) + PsiElement('{')('{') + RuleSpecNode(objectProperties_) + ObjectPropertyDeclarationNode(ast_objectProperty) + ObjectPropertyAssignmentNode(propertyAssignment) + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement(',')(',') + ObjectPropertyDeclarationNode(ast_objectProperty) + ObjectPropertyAssignmentNode(propertyAssignment) + RuleSpecNode(expression_propName) + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement(':')(':') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement('}')('}') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiElement(SingleLineComment)('//') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('fn1') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + ArrowFunctionNode(arrowFunctionExpression) + PsiElement('(')('(') + RuleSpecNode(parameterList_) + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement(',')(',') + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement(')')(')') + PsiElement('=>')('=>') + ArrowFunctionBodyNode(expression_functionBody) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('+')('+') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + RuleSpecNode(eos__) + PsiElement(';')(';') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + FunctionDeclarationNode(functionDeclaration) + PsiElement('function')('function') + IdentifierNode(identifier) + PsiElement(Identifier)('fn2') + PsiElement('(')('(') + RuleSpecNode(parameterList_) + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement(',')(',') + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement(')')(')') + BlockStatementNode(blockStatement) + PsiElement('{')('{') + RuleSpecNode(statements_) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('5') + RuleSpecNode(eos__) + PsiElement(';')(';') + RuleSpecNode(statement) + RuleSpecNode(returnStatement) + PsiElement('return')('return') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('+')('+') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement('+')('+') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiElement('}')('}') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + FunctionDeclarationNode(functionDeclaration) + PsiElement('function')('function') + IdentifierNode(identifier) + PsiElement(Identifier)('fn3') + PsiElement('(')('(') + RuleSpecNode(parameterList_) + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + RuleSpecNode(namedTypeNode_annotation) + PsiElement(':')(':') + RuleSpecNode(namedTypeNode) + RuleSpecNode(typeNameNode_predefined) + PsiElement('string')('string') + PsiElement(',')(',') + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + RuleSpecNode(namedTypeNode_annotation) + PsiElement(':')(':') + RuleSpecNode(namedTypeNode) + RuleSpecNode(typeNameNode_predefined) + PsiElement('number')('number') + PsiElement(')')(')') + BlockStatementNode(blockStatement) + PsiElement('{')('{') + RuleSpecNode(statements_) + RuleSpecNode(statement) + RuleSpecNode(returnStatement) + PsiElement('return')('return') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('+')('+') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiElement('}')('}') + PsiElement(SingleLineComment)('//') + StatementRootNode(ast_topLevelStatement) + RuleSpecNode(statement) + RuleSpecNode(ifStatement) + PsiElement('if')('if') + PsiElement('(')('(') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('>')('>') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('2') + PsiElement(')')(')') + RuleSpecNode(statement) + BlockStatementNode(blockStatement) + PsiElement('{')('{') + RuleSpecNode(statements_) + RuleSpecNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + RuleSpecNode(variableDeclarators_) + RuleSpecNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('3') + RuleSpecNode(eos__) + PsiElement(';')(';') + RuleSpecNode(statement) + RuleSpecNode(expressionStatement) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement(',')(',') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiElement('}')('}') diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-2.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-2.ast deleted file mode 100644 index d97bde885..000000000 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-2.ast +++ /dev/null @@ -1,45 +0,0 @@ -FILE(0,50) - ProgramNode(program)(0,49) - RuleSpecNode(topLevelStatements_)(0,49) - RuleSpecNode(ast_topLevelStatement)(0,24) - RuleSpecNode(moduleDeclaration_import)(0,24) - ImportAsDeclarationNode(importAsDeclaration)(0,24) - PsiElement('import')('import')(0,6) - ImportSourceNode(ast_importSource)(7,23) - ImportQualifiedNameNode(qualifiedName)(7,23) - RuleSpecNode(qualifiedName_name_)(7,11) - RuleSpecNode(identifier)(7,11) - PsiElement(Identifier)('java')(7,11) - PsiElement('.')('.')(11,12) - ImportQualifiedNameNode(qualifiedName)(12,23) - RuleSpecNode(qualifiedName_name_)(12,16) - RuleSpecNode(identifier)(12,16) - PsiElement(Identifier)('lang')(12,16) - PsiElement('.')('.')(16,17) - ImportQualifiedNameNode(qualifiedName)(17,23) - RuleSpecNode(qualifiedName_name_)(17,23) - RuleSpecNode(identifier)(17,23) - PsiElement(Identifier)('String')(17,23) - RuleSpecNode(eos__)(23,24) - PsiElement(';')(';')(23,24) - RuleSpecNode(ast_topLevelStatement)(25,49) - RuleSpecNode(moduleDeclaration_import)(25,49) - ImportAsDeclarationNode(importAsDeclaration)(25,49) - PsiElement('import')('import')(25,31) - ImportSourceNode(ast_importSource)(32,48) - ImportQualifiedNameNode(qualifiedName)(32,48) - RuleSpecNode(qualifiedName_name_)(32,36) - RuleSpecNode(identifier)(32,36) - PsiElement(Identifier)('java')(32,36) - PsiElement('.')('.')(36,37) - ImportQualifiedNameNode(qualifiedName)(37,48) - RuleSpecNode(qualifiedName_name_)(37,41) - RuleSpecNode(identifier)(37,41) - PsiElement(Identifier)('lang')(37,41) - PsiElement('.')('.')(41,42) - ImportQualifiedNameNode(qualifiedName)(42,48) - RuleSpecNode(qualifiedName_name_)(42,48) - RuleSpecNode(identifier)(42,48) - PsiElement(Identifier)('Number')(42,48) - RuleSpecNode(eos__)(48,49) - PsiElement(';')(';')(48,49) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-3.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-3.ast deleted file mode 100644 index 15264d2c3..000000000 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-3.ast +++ /dev/null @@ -1,92 +0,0 @@ -FILE(0,98) - ProgramNode(program)(0,97) - RuleSpecNode(topLevelStatements_)(0,97) - RuleSpecNode(ast_topLevelStatement)(0,24) - RuleSpecNode(moduleDeclaration_import)(0,24) - ImportAsDeclarationNode(importAsDeclaration)(0,24) - PsiElement('import')('import')(0,6) - ImportSourceNode(ast_importSource)(7,23) - ImportQualifiedNameNode(qualifiedName)(7,23) - RuleSpecNode(qualifiedName_name_)(7,11) - RuleSpecNode(identifier)(7,11) - PsiElement(Identifier)('java')(7,11) - PsiElement('.')('.')(11,12) - ImportQualifiedNameNode(qualifiedName)(12,23) - RuleSpecNode(qualifiedName_name_)(12,16) - RuleSpecNode(identifier)(12,16) - PsiElement(Identifier)('lang')(12,16) - PsiElement('.')('.')(16,17) - ImportQualifiedNameNode(qualifiedName)(17,23) - RuleSpecNode(qualifiedName_name_)(17,23) - RuleSpecNode(identifier)(17,23) - PsiElement(Identifier)('String')(17,23) - RuleSpecNode(eos__)(23,24) - PsiElement(';')(';')(23,24) - RuleSpecNode(ast_topLevelStatement)(25,49) - RuleSpecNode(moduleDeclaration_import)(25,49) - ImportAsDeclarationNode(importAsDeclaration)(25,49) - PsiElement('import')('import')(25,31) - ImportSourceNode(ast_importSource)(32,48) - ImportQualifiedNameNode(qualifiedName)(32,48) - RuleSpecNode(qualifiedName_name_)(32,36) - RuleSpecNode(identifier)(32,36) - PsiElement(Identifier)('java')(32,36) - PsiElement('.')('.')(36,37) - ImportQualifiedNameNode(qualifiedName)(37,48) - RuleSpecNode(qualifiedName_name_)(37,41) - RuleSpecNode(identifier)(37,41) - PsiElement(Identifier)('lang')(37,41) - PsiElement('.')('.')(41,42) - ImportQualifiedNameNode(qualifiedName)(42,48) - RuleSpecNode(qualifiedName_name_)(42,48) - RuleSpecNode(identifier)(42,48) - PsiElement(Identifier)('Number')(42,48) - RuleSpecNode(eos__)(48,49) - PsiElement(';')(';')(48,49) - RuleSpecNode(ast_topLevelStatement)(50,80) - RuleSpecNode(statement)(50,80) - RuleSpecNode(variableDeclaration)(50,80) - RuleSpecNode(varModifier_)(50,55) - PsiElement('const')('const')(50,55) - RuleSpecNode(variableDeclarators_)(56,79) - RuleSpecNode(variableDeclarator)(56,79) - RuleSpecNode(ast_identifierOrPattern)(56,59) - RuleSpecNode(identifier)(56,59) - PsiElement(Identifier)('abc')(56,59) - RuleSpecNode(expression_initializer)(60,79) - PsiElement('=')('=')(60,61) - RuleSpecNode(expression_single)(62,79) - PsiElement('new')('new')(62,65) - RuleSpecNode(parameterizedTypeNode)(66,72) - RuleSpecNode(qualifiedName_)(66,72) - ImportQualifiedNameNode(qualifiedName)(66,72) - RuleSpecNode(qualifiedName_name_)(66,72) - RuleSpecNode(identifier)(66,72) - PsiElement(Identifier)('String')(66,72) - RuleSpecNode(arguments_)(72,79) - PsiElement('(')('(')(72,73) - RuleSpecNode(expression_single)(73,78) - RuleSpecNode(literal)(73,78) - RuleSpecNode(literal_string)(73,78) - PsiElement(StringLiteral)('"abc"')(73,78) - PsiElement(')')(')')(78,79) - RuleSpecNode(eos__)(79,80) - PsiElement(';')(';')(79,80) - RuleSpecNode(ast_topLevelStatement)(81,97) - RuleSpecNode(statement)(81,97) - RuleSpecNode(variableDeclaration)(81,97) - RuleSpecNode(varModifier_)(81,86) - PsiElement('const')('const')(81,86) - RuleSpecNode(variableDeclarators_)(87,96) - RuleSpecNode(variableDeclarator)(87,96) - RuleSpecNode(ast_identifierOrPattern)(87,90) - RuleSpecNode(identifier)(87,90) - PsiElement(Identifier)('def')(87,90) - RuleSpecNode(expression_initializer)(91,96) - PsiElement('=')('=')(91,92) - RuleSpecNode(expression_single)(93,96) - RuleSpecNode(literal)(93,96) - RuleSpecNode(literal_numeric)(93,96) - PsiElement(DecimalIntegerLiteral)('123')(93,96) - RuleSpecNode(eos__)(96,97) - PsiElement(';')(';')(96,97) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-4.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-4.ast deleted file mode 100644 index 9b51459c7..000000000 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-4.ast +++ /dev/null @@ -1,142 +0,0 @@ -FILE(0,152) - ProgramNode(program)(0,151) - RuleSpecNode(topLevelStatements_)(0,151) - RuleSpecNode(ast_topLevelStatement)(0,24) - RuleSpecNode(moduleDeclaration_import)(0,24) - ImportAsDeclarationNode(importAsDeclaration)(0,24) - PsiElement('import')('import')(0,6) - ImportSourceNode(ast_importSource)(7,23) - ImportQualifiedNameNode(qualifiedName)(7,23) - RuleSpecNode(qualifiedName_name_)(7,11) - RuleSpecNode(identifier)(7,11) - PsiElement(Identifier)('java')(7,11) - PsiElement('.')('.')(11,12) - ImportQualifiedNameNode(qualifiedName)(12,23) - RuleSpecNode(qualifiedName_name_)(12,16) - RuleSpecNode(identifier)(12,16) - PsiElement(Identifier)('lang')(12,16) - PsiElement('.')('.')(16,17) - ImportQualifiedNameNode(qualifiedName)(17,23) - RuleSpecNode(qualifiedName_name_)(17,23) - RuleSpecNode(identifier)(17,23) - PsiElement(Identifier)('Number')(17,23) - RuleSpecNode(eos__)(23,24) - PsiElement(';')(';')(23,24) - RuleSpecNode(ast_topLevelStatement)(25,53) - RuleSpecNode(statement)(25,53) - RuleSpecNode(variableDeclaration)(25,53) - RuleSpecNode(varModifier_)(25,30) - PsiElement('const')('const')(25,30) - RuleSpecNode(variableDeclarators_)(31,52) - RuleSpecNode(variableDeclarator)(31,52) - RuleSpecNode(ast_identifierOrPattern)(31,34) - RuleSpecNode(identifier)(31,34) - PsiElement(Identifier)('fn1')(31,34) - RuleSpecNode(expression_initializer)(35,52) - PsiElement('=')('=')(35,36) - RuleSpecNode(expression_single)(37,52) - RuleSpecNode(arrowFunctionExpression)(37,52) - PsiElement('(')('(')(37,38) - RuleSpecNode(parameterList_)(38,42) - RuleSpecNode(parameterDeclaration)(38,39) - RuleSpecNode(ast_identifierOrPattern)(38,39) - RuleSpecNode(identifier)(38,39) - PsiElement(Identifier)('a')(38,39) - PsiElement(',')(',')(39,40) - RuleSpecNode(parameterDeclaration)(41,42) - RuleSpecNode(ast_identifierOrPattern)(41,42) - RuleSpecNode(identifier)(41,42) - PsiElement(Identifier)('b')(41,42) - PsiElement(')')(')')(42,43) - PsiElement('=>')('=>')(44,46) - RuleSpecNode(expression_functionBody)(47,52) - RuleSpecNode(expression_single)(47,52) - RuleSpecNode(expression_single)(47,48) - RuleSpecNode(identifier)(47,48) - PsiElement(Identifier)('a')(47,48) - PsiElement('+')('+')(49,50) - RuleSpecNode(expression_single)(51,52) - RuleSpecNode(identifier)(51,52) - PsiElement(Identifier)('b')(51,52) - RuleSpecNode(eos__)(52,53) - PsiElement(';')(';')(52,53) - RuleSpecNode(ast_topLevelStatement)(54,94) - RuleSpecNode(statement)(54,94) - RuleSpecNode(functionDeclaration)(54,94) - PsiElement('function')('function')(54,62) - RuleSpecNode(identifier)(63,66) - PsiElement(Identifier)('fn2')(63,66) - PsiElement('(')('(')(66,67) - RuleSpecNode(parameterList_)(67,71) - RuleSpecNode(parameterDeclaration)(67,68) - RuleSpecNode(ast_identifierOrPattern)(67,68) - RuleSpecNode(identifier)(67,68) - PsiElement(Identifier)('a')(67,68) - PsiElement(',')(',')(68,69) - RuleSpecNode(parameterDeclaration)(70,71) - RuleSpecNode(ast_identifierOrPattern)(70,71) - RuleSpecNode(identifier)(70,71) - PsiElement(Identifier)('b')(70,71) - PsiElement(')')(')')(71,72) - RuleSpecNode(blockStatement)(73,94) - PsiElement('{')('{')(73,74) - RuleSpecNode(statements_)(79,92) - RuleSpecNode(statement)(79,92) - RuleSpecNode(returnStatement)(79,92) - PsiElement('return')('return')(79,85) - RuleSpecNode(expression_single)(86,91) - RuleSpecNode(expression_single)(86,87) - RuleSpecNode(identifier)(86,87) - PsiElement(Identifier)('a')(86,87) - PsiElement('+')('+')(88,89) - RuleSpecNode(expression_single)(90,91) - RuleSpecNode(identifier)(90,91) - PsiElement(Identifier)('b')(90,91) - RuleSpecNode(eos__)(91,92) - PsiElement(';')(';')(91,92) - PsiElement('}')('}')(93,94) - RuleSpecNode(ast_topLevelStatement)(95,151) - RuleSpecNode(statement)(95,151) - RuleSpecNode(functionDeclaration)(95,151) - PsiElement('function')('function')(95,103) - RuleSpecNode(identifier)(104,107) - PsiElement(Identifier)('fn3')(104,107) - PsiElement('(')('(')(107,108) - RuleSpecNode(parameterList_)(108,128) - RuleSpecNode(parameterDeclaration)(108,117) - RuleSpecNode(ast_identifierOrPattern)(108,109) - RuleSpecNode(identifier)(108,109) - PsiElement(Identifier)('a')(108,109) - RuleSpecNode(namedTypeNode_annotation)(109,117) - PsiElement(':')(':')(109,110) - RuleSpecNode(namedTypeNode)(111,117) - RuleSpecNode(typeNameNode_predefined)(111,117) - PsiElement('string')('string')(111,117) - PsiElement(',')(',')(117,118) - RuleSpecNode(parameterDeclaration)(119,128) - RuleSpecNode(ast_identifierOrPattern)(119,120) - RuleSpecNode(identifier)(119,120) - PsiElement(Identifier)('b')(119,120) - RuleSpecNode(namedTypeNode_annotation)(120,128) - PsiElement(':')(':')(120,121) - RuleSpecNode(namedTypeNode)(122,128) - RuleSpecNode(typeNameNode_predefined)(122,128) - PsiElement('number')('number')(122,128) - PsiElement(')')(')')(128,129) - RuleSpecNode(blockStatement)(130,151) - PsiElement('{')('{')(130,131) - RuleSpecNode(statements_)(136,149) - RuleSpecNode(statement)(136,149) - RuleSpecNode(returnStatement)(136,149) - PsiElement('return')('return')(136,142) - RuleSpecNode(expression_single)(143,148) - RuleSpecNode(expression_single)(143,144) - RuleSpecNode(identifier)(143,144) - PsiElement(Identifier)('a')(143,144) - PsiElement('+')('+')(145,146) - RuleSpecNode(expression_single)(147,148) - RuleSpecNode(identifier)(147,148) - PsiElement(Identifier)('b')(147,148) - RuleSpecNode(eos__)(148,149) - PsiElement(';')(';')(148,149) - PsiElement('}')('}')(150,151) diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast deleted file mode 100644 index 993a6c875..000000000 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-5.ast +++ /dev/null @@ -1,190 +0,0 @@ -FILE - ProgramNode(program) - RuleSpecNode(topLevelStatements_) - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) - VariableDeclarationNode(variableDeclaration) - RuleSpecNode(varModifier_) - PsiElement('const')('const') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) - RuleSpecNode(ast_identifierOrPattern) - IdentifierNode(identifier) - PsiElement(Identifier)('abc') - RuleSpecNode(expression_initializer) - PsiElement('=')('=') - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('ormTemplate') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('findListByQuery') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('query') - PsiElement(',')(',') - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('mapper') - PsiElement(')')(')') - RuleSpecNode(eos__) - PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) - RuleSpecNode(expressionStatement) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('query') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('addFilter') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('filter') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('query') - PsiElement(',')(',') - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('svcCtx') - PsiElement(')')(')') - PsiElement(')')(')') - RuleSpecNode(eos__) - PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) - RuleSpecNode(expressionStatement) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('1') - PsiElement(',')(',') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('2') - PsiElement(')')(')') - RuleSpecNode(eos__) - PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) - RuleSpecNode(expressionStatement) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('b') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('c') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('1') - PsiElement(',')(',') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('2') - PsiElement(')')(')') - RuleSpecNode(eos__) - PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) - VariableDeclarationNode(variableDeclaration) - RuleSpecNode(varModifier_) - PsiElement('const')('const') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) - RuleSpecNode(ast_identifierOrPattern) - IdentifierNode(identifier) - PsiElement(Identifier)('c') - RuleSpecNode(expression_initializer) - PsiElement('=')('=') - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('b') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('c') - RuleSpecNode(eos__) - PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) - VariableDeclarationNode(variableDeclaration) - RuleSpecNode(varModifier_) - PsiElement('const')('const') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) - RuleSpecNode(ast_identifierOrPattern) - IdentifierNode(identifier) - PsiElement(Identifier)('def') - RuleSpecNode(expression_initializer) - PsiElement('=')('=') - ExpressionNode(expression_single) - ObjectDeclarationNode(objectExpression) - PsiElement('{')('{') - RuleSpecNode(objectProperties_) - ObjectPropertyDeclarationNode(ast_objectProperty) - ObjectPropertyAssignmentNode(propertyAssignment) - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - PsiElement(',')(',') - ObjectPropertyDeclarationNode(ast_objectProperty) - ObjectPropertyAssignmentNode(propertyAssignment) - RuleSpecNode(expression_propName) - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('b') - PsiElement(':')(':') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('1') - PsiElement('}')('}') - RuleSpecNode(eos__) - PsiElement(';')(';') -- Gitee From 26172b1fd10873a19cf7a0f884f0bf48f8706e2f Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 3 Jul 2025 18:08:29 +0800 Subject: [PATCH 32/82] =?UTF-8?q?nop-idea-pugin:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=8E=20XLang=20Script=20=E4=B8=AD=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=92=8C=E5=B1=9E=E6=80=A7=E7=9A=84=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E6=9E=84=E9=80=A0=E7=9B=B8=E5=85=B3=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../script/XLangScriptParserDefinition.java | 6 ++ .../lang/script/psi/ExpressionNode.java | 23 ++--- .../plugin/lang/script/psi/LiteralNode.java | 3 +- .../plugin/lang/script/psi/RuleSpecNode.java | 18 +++- .../plugin/lang/script/psi/StatementNode.java | 18 +--- .../script/psi/VariableDeclarationNode.java | 27 +++++ .../script/psi/VariableDeclaratorNode.java | 42 ++++++++ .../script/psi/VariableDeclaratorsNode.java | 46 +++++++++ .../nop/idea/plugin/utils/PsiClassHelper.java | 99 +++++++++++++++++++ .../idea/plugin/BaseXLangPluginTestCase.java | 20 +++- .../plugin/lang/TestXLangScriptParser.java | 4 +- .../lang/TestXLangScriptReferences.java | 54 +++++++++- .../_vfs/test/java/XJsonDomainHandler.java | 5 + .../test/resources/_vfs/test/reference/a.xlib | 1 + 14 files changed, 332 insertions(+), 34 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index 65742c238..97bb50e39 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -34,6 +34,8 @@ import io.nop.idea.plugin.lang.script.psi.RuleSpecNode; import io.nop.idea.plugin.lang.script.psi.StatementNode; import io.nop.idea.plugin.lang.script.psi.TopLevelStatementNode; import io.nop.idea.plugin.lang.script.psi.VariableDeclarationNode; +import io.nop.idea.plugin.lang.script.psi.VariableDeclaratorNode; +import io.nop.idea.plugin.lang.script.psi.VariableDeclaratorsNode; import io.nop.xlang.parse.antlr.XLangLexer; import io.nop.xlang.parse.antlr.XLangParser; import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; @@ -113,6 +115,10 @@ public class XLangScriptParserDefinition implements ParserDefinition { // case XLangParser.RULE_variableDeclaration -> // new VariableDeclarationNode(node); + case XLangParser.RULE_variableDeclarators_ -> // + new VariableDeclaratorsNode(node); + case XLangParser.RULE_variableDeclarator -> // + new VariableDeclaratorNode(node); case XLangParser.RULE_blockStatement -> // new BlockStatementNode(node); // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index 4655e8397..3c9ebf6af 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -251,23 +251,23 @@ public class ExpressionNode extends RuleSpecNode { /** 获取对象的方法 */ protected PsiMethod @NotNull [] getObjectMethods() { - return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findMethodsByName(objClass, - memberName, - true)); + return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findMethodsByName(objClass, memberName, true), + PsiMethod.EMPTY_ARRAY); } /** 获取对象的属性 */ protected PsiField getObjectProperty() { - return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findFieldByName(objClass, memberName, true)); + return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findFieldByName(objClass, memberName, true), + null); } - protected T getObjectMember(BiFunction consumer) { + protected T getObjectMember(BiFunction consumer, T defaultValue) { ExpressionNode obj = (ExpressionNode) getFirstChild(); ObjectMemberNode member = (ObjectMemberNode) getLastChild(); PsiClass objClass = obj.getResultType(); if (objClass == null) { - return null; + return defaultValue; } String memberName = member.getText(); @@ -314,12 +314,13 @@ public class ExpressionNode extends RuleSpecNode { PsiMethod method = getObjectMethod(); PsiType returnType = method != null ? method.getReturnType() : null; - if (returnType != null) { - String typeName = returnType.getCanonicalText(false); + return PsiClassHelper.getTypeClass(getProject(), returnType); + } // + else if (isObjectMember()) { + PsiField prop = getObjectProperty(); + PsiType propType = prop != null ? prop.getType() : null; - return PsiClassHelper.findClass(getProject(), typeName); - } - return null; + return PsiClassHelper.getTypeClass(getProject(), propType); } // else if (isObjectConstructorCall()) { ParameterizedTypeNode cst = PsiTreeUtil.findChildOfType(this, ParameterizedTypeNode.class); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java index d1137951f..e04433655 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java @@ -5,6 +5,7 @@ import java.util.regex.Pattern; import com.intellij.lang.ASTNode; import com.intellij.psi.PsiClass; import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiTreeUtil; import io.nop.idea.plugin.utils.PsiClassHelper; import io.nop.xlang.parse.antlr.XLangLexer; import org.antlr.intellij.adaptor.lexer.TokenIElementType; @@ -24,7 +25,7 @@ public class LiteralNode extends RuleSpecNode { /** 获取字面量的数据类型 */ public PsiClass getDataType() { - LeafPsiElement target = (LeafPsiElement) getFirstChild().getFirstChild(); + LeafPsiElement target = (LeafPsiElement) PsiTreeUtil.getDeepestLast(this); String typeName = switch (((TokenIElementType) target.getElementType()).getANTLRTokenType()) { case XLangLexer.NullLiteral // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java index 1dcdfb068..66fa49e35 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java @@ -9,6 +9,7 @@ import com.intellij.openapi.application.ReadAction; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; +import org.antlr.intellij.adaptor.lexer.RuleIElementType; import org.antlr.intellij.adaptor.psi.Trees; import org.jetbrains.annotations.NotNull; @@ -22,6 +23,7 @@ public class RuleSpecNode extends ASTWrapperPsiElement { super(node); } + /** @return 不含注释和空白节点 */ @Override public PsiElement @NotNull [] getChildren() { return Trees.getChildren(this); @@ -33,6 +35,14 @@ public class RuleSpecNode extends ASTWrapperPsiElement { return ReadAction.compute(this::doGetReferences); } + public IdentifierNode getIdentifier() { + return findChildByClass(IdentifierNode.class); + } + + public boolean isRuleNode(int ruleIndex) { + return ((RuleIElementType) getNode().getElementType()).getRuleIndex() == ruleIndex; + } + /** 获取当前节点可访问到的变量及其类型 */ public @NotNull Map getVisibleVarTypes() { Map types = new HashMap<>(); @@ -44,8 +54,12 @@ public class RuleSpecNode extends ASTWrapperPsiElement { // Note: 下层的变量优先于上层的变量 if (parent instanceof RuleSpecNode) { for (PsiElement child : parent.getChildren()) { - if (child != node) { - ((RuleSpecNode) child).getVarTypes().forEach(types::putIfAbsent); + if (child == node) { + break; // 只取当前节点之前定义的变量 + } + + if (child instanceof RuleSpecNode c) { + c.getVarTypes().forEach(types::putIfAbsent); } } } else { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java index f2d399637..5cb078ddf 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java @@ -3,7 +3,6 @@ package io.nop.idea.plugin.lang.script.psi; import java.util.Map; import com.intellij.lang.ASTNode; -import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import org.jetbrains.annotations.NotNull; @@ -19,8 +18,6 @@ public class StatementNode extends RuleSpecNode { @Override public @NotNull Map getVarTypes() { - PsiElement firstChild = getFirstChild(); - /* 变量声明:let def = 123; StatementNode(statement) VariableDeclarationNode(variableDeclaration) @@ -40,18 +37,11 @@ public class StatementNode extends RuleSpecNode { RuleSpecNode(eos__) PsiElement(';')(';') */ - if (firstChild instanceof VariableDeclarationNode declaration) { - RuleSpecNode declarator = (RuleSpecNode) declaration.getLastChild().getFirstChild(); - IdentifierNode identifier = (IdentifierNode) declarator.getFirstChild().getFirstChild(); - ExpressionNode expression = (ExpressionNode) declarator.getLastChild().getLastChild(); - - String varName = identifier.getText(); - PsiClass varType = expression.getResultType(); - VarDecl varDecl = new VarDecl(identifier, varType); - - return Map.of(varName, varDecl); + for (PsiElement child : getChildren()) { + if (child instanceof VariableDeclarationNode declaration) { + return declaration.getVarTypes(); + } } - return Map.of(); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java index 70a46d77d..9187bcbde 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java @@ -1,5 +1,7 @@ package io.nop.idea.plugin.lang.script.psi; +import java.util.Map; + import com.intellij.lang.ASTNode; import org.jetbrains.annotations.NotNull; @@ -14,4 +16,29 @@ public class VariableDeclarationNode extends RuleSpecNode { public VariableDeclarationNode(@NotNull ASTNode node) { super(node); } + + @Override + public @NotNull Map getVarTypes() { + /* 变量声明:let def = 123; + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('def') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('123') + RuleSpecNode(eos__) + PsiElement(';')(';') + */ + VariableDeclaratorsNode declarators = findChildByClass(VariableDeclaratorsNode.class); + + return declarators.getVarTypes(); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java new file mode 100644 index 000000000..3f7d76541 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java @@ -0,0 +1,42 @@ +package io.nop.idea.plugin.lang.script.psi; + +import java.util.Map; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-07-03 + */ +public class VariableDeclaratorNode extends RuleSpecNode { + + public VariableDeclaratorNode(@NotNull ASTNode node) { + super(node); + } + + @Override + public @NotNull Map getVarTypes() { + /* 变量声明:def = 123; + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('def') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('123') + */ + IdentifierNode identifier = ((RuleSpecNode) getFirstChild()).getIdentifier(); + ExpressionNode expression = (ExpressionNode) getLastChild().getLastChild(); + + String varName = identifier.getText(); + PsiClass varType = expression.getResultType(); + VarDecl varDecl = new VarDecl(identifier, varType); + + return Map.of(varName, varDecl); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java new file mode 100644 index 000000000..96da9d311 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java @@ -0,0 +1,46 @@ +package io.nop.idea.plugin.lang.script.psi; + +import java.util.HashMap; +import java.util.Map; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; + +/** + * 由逗号分隔的多个变量声明的节点 + * + * @author flytreeleft + * @date 2025-07-03 + */ +public class VariableDeclaratorsNode extends RuleSpecNode { + + public VariableDeclaratorsNode(@NotNull ASTNode node) { + super(node); + } + + @Override + public @NotNull Map getVarTypes() { + /* 变量声明:abc = 123, def = 456; + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('123') + */ + Map vars = new HashMap<>(); + + for (PsiElement child : getChildren()) { + if (child instanceof VariableDeclaratorNode declarator) { + vars.putAll(declarator.getVarTypes()); + } + } + return vars; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java index f74e84284..e19138521 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -7,10 +7,13 @@ import java.util.Map; import java.util.Objects; import com.intellij.openapi.project.Project; +import com.intellij.psi.CommonClassNames; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.JavaRecursiveElementVisitor; import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiArrayType; import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; import com.intellij.psi.PsiConstantEvaluationHelper; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementFactory; @@ -20,10 +23,16 @@ import com.intellij.psi.PsiLiteralExpression; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifier; import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiPrimitiveType; import com.intellij.psi.PsiReferenceExpression; import com.intellij.psi.PsiReturnStatement; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiTypeParameter; +import com.intellij.psi.PsiTypes; +import com.intellij.psi.PsiWildcardType; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.searches.ClassInheritorsSearch; +import com.intellij.psi.util.PsiUtil; import com.intellij.util.EmptyQuery; import com.intellij.util.Query; import io.nop.commons.util.StringHelper; @@ -35,6 +44,96 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-26 */ public class PsiClassHelper { + private static final Map primitiveTypeWrapper = Map.of(PsiTypes.byteType(), + "java.lang.Byte", + PsiTypes.shortType(), + "java.lang.Short", + PsiTypes.intType(), + "java.lang.Integer", + PsiTypes.longType(), + "java.lang.Long", + PsiTypes.floatType(), + "java.lang.Float", + PsiTypes.doubleType(), + "java.lang.Double", + PsiTypes.charType(), + "java.lang.Character", + PsiTypes.booleanType(), + "java.lang.Boolean", + PsiTypes.voidType(), + "java.lang.Void"); + + /** 得到 {@link PsiType} 对应的 {@link PsiClass} */ + public static PsiClass getTypeClass(Project project, PsiType type) { + if (type == null) { + return null; + } + +// String typeName = type.getCanonicalText(false); +// return PsiClassHelper.findClass(project, typeName); + + // 处理通配符泛型 + if (type instanceof PsiWildcardType t) { + PsiType bound = t.getBound(); + + return bound != null + ? getTypeClass(project, bound) + : PsiUtil.resolveClassInType(PsiType.getJavaLangObject(t.getManager(), t.getResolveScope())); + } + // 处理类型参数 + else if (type instanceof PsiTypeParameter t) { + PsiClassType[] bounds = t.getExtendsListTypes(); + + if (bounds.length > 0) { + return getTypeClass(project, bounds[0]); + } + return PsiUtil.resolveClassInType(PsiType.getJavaLangObject(t.getManager(), t.getResolveScope())); + } + // 处理原始类型 + else if (type instanceof PsiPrimitiveType t) { + String wrapperName = primitiveTypeWrapper.get(t); + + if (wrapperName != null) { + return JavaPsiFacade.getInstance(project).findClass(wrapperName, GlobalSearchScope.allScope(project)); + } + return null; + } + // 处理数组类型 + else if (type instanceof PsiArrayType t) { + return getTypeClass(project, t.getComponentType()); + } + // 处理类类型(包括泛型) + else if (type instanceof PsiClassType t) { + PsiClass cls = t.resolve(); + // 泛型参数 + PsiType[] parameters = t.getParameters(); + + if (cls != null && parameters.length > 0) { + // List -> 返回 String.class + if (CommonClassNames.JAVA_UTIL_LIST.equals(cls.getQualifiedName())) { + return getTypeClass(project, parameters[0]); + } + + // 自定义泛型类 + PsiTypeParameter[] typeParams = cls.getTypeParameters(); + if (typeParams.length > 0) { + // 查找实际使用的类型参数 + for (int i = 0; i < typeParams.length; i++) { + if (i < parameters.length) { + PsiClass resolved = getTypeClass(project, parameters[i]); + + if (resolved != null) { + return resolved; + } + } + } + } + } + return cls; + } + + return null; + } /** * 查找项目中 {@link io.nop.xlang.xdef.IStdDomainHandler IStdDomainHandler} 的实现类, diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 323b82603..c73f25d8a 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -19,6 +19,10 @@ import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.projectRoots.ProjectJdkTable; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.impl.JavaSdkImpl; +import com.intellij.openapi.roots.ModuleRootModificationUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; @@ -48,6 +52,17 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur // Note: 消除异常 "Write access is allowed inside write-action only" ApplicationManager.getApplication().runWriteAction(() -> { + // 将真实的 JDK 注入到项目中,以确保能够通过 JavaPsiFacade 查找得到 JDK class + String jdkHome = System.getProperty("java.home"); + Sdk jdk = JavaSdkImpl.getInstance().createJdk("System JDK", jdkHome, true); + + ProjectJdkTable.getInstance().addJdk(jdk); + ModuleRootModificationUtil.setModuleSdk(getModule(), jdk); + + cleanup.appendOnCancelTask(() -> { + ProjectJdkTable.getInstance().removeJdk(jdk); + }); + // Note: *.xdef 等需显式注册,否则,这类文件会被视为二进制文件, // 在通过 PsiDocumentManager 获取 Document 时,将返回 null FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, "xdef"); @@ -65,7 +80,10 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur @Override protected void tearDown() throws Exception { - cleanup.cancel(); + ApplicationManager.getApplication().runWriteAction(() -> { + cleanup.cancel(); + }); + super.tearDown(); } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index ffd16b5dc..a64f4fbcd 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -80,7 +80,7 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { assertEquals("", testTree); } - protected String toParseTreeText(@NotNull PsiElement file) { - return DebugUtil.psiToString(file, false, false); + protected String toParseTreeText(@NotNull PsiElement tree) { + return DebugUtil.psiToString(tree, false, false); } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java index 7ee97295d..35eb57639 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -9,6 +9,8 @@ package io.nop.idea.plugin.lang; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiPackage; import com.intellij.psi.PsiReference; import io.nop.idea.plugin.BaseXLangPluginTestCase; @@ -35,11 +37,47 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { /** 测试对对象成员的引用 */ public void testObjectMemberReference() { + assertReference(""" + "abc".trim(); + """, "java.lang.String#trim"); + assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler(); handler.getName(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName()"); + // 尝试触发无限递归 + let name = handler.getName(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); + + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const handler = new XJsonDomainHandler(); + handler.instance().getName(); + // 尝试触发无限递归 + let name = handler.getName(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); + + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const handler = XJsonDomainHandler.INSTANCE; + // 尝试触发无限递归 + let name = handler.getName(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#INSTANCE"); + + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + let name = XJsonDomainHandler.INSTANCE.getName(); + // 尝试触发无限递归 + const handler = new XJsonDomainHandler(); + name = handler.getName(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); + } + + public void testVarReference() { + assertReference(""" + let abc = "abc"; + const def = abc + "def"; + """, ""); } public void testJavaClassMemberReference() { @@ -73,10 +111,20 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { if (target instanceof PsiClass cls) { String actual = cls.getQualifiedName(); assertEquals(expected, actual); - } else if (target instanceof PsiPackage pkg) { + } // + else if (target instanceof PsiMethod method) { + String actual = method.getContainingClass().getQualifiedName() + "#" + method.getName(); + assertEquals(expected, actual); + } // + else if (target instanceof PsiField field) { + String actual = field.getContainingClass().getQualifiedName() + "#" + field.getName(); + assertEquals(expected, actual); + } // + else if (target instanceof PsiPackage pkg) { String actual = pkg.getQualifiedName(); assertEquals(expected, actual); - } else { + } // + else { fail("Unknown target " + target); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java index 6695b2a4a..5a0fc289a 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java @@ -3,9 +3,14 @@ package io.nop.xlang.xdef.domain; import io.nop.xlang.xdef.IStdDomainHandler; public class XJsonDomainHandler implements IStdDomainHandler { + public static final XJsonDomainHandler INSTANCE = new XJsonDomainHandler(); @Override public String getName() { return "xjson"; } + + public XJsonDomainHandler instance() { + return INSTANCE; + } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index b42753f67..22bf9a3a2 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -18,6 +18,7 @@ const query = QueryBeanHelper.buildQueryBeanFromTreeBean(queryNode); const ormTemplate = inject('nopOrmTemplate'); const mapper = ormTemplate.getRowMapper(rowType,false); + const s = "abc".startsWith("a"); while (true) { let a = 's' instanceof string; -- Gitee From 0e5cf25f4a4cce79416cf98a0c0b7f30b2dd1233 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 4 Jul 2025 17:02:37 +0800 Subject: [PATCH 33/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E5=96=84=20XLang=20Script=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/nop/idea/plugin/lang/XLangVarDecl.java | 12 + .../nop/idea/plugin/lang/XLangVarScope.java | 19 + .../lang/script/XLangScriptASTFactory.java | 15 +- .../script/XLangScriptParserDefinition.java | 45 +- .../lang/script/XLangScriptTokenTypes.java | 59 ++ .../XLangScriptCompletionProvider.java | 2 +- .../lang/script/psi/ArrayExpressionNode.java | 46 ++ .../script/psi/AssignmentExpressionNode.java | 73 +++ .../lang/script/psi/CalleeArgumentsNode.java | 30 + .../lang/script/psi/ExpressionNode.java | 582 ++++++++++++------ .../script/psi/FunctionDeclarationNode.java | 72 +++ .../lang/script/psi/IdentifierNode.java | 53 +- .../script/psi/ImportDeclarationNode.java | 76 ++- .../lang/script/psi/ImportSourceNode.java | 11 +- .../plugin/lang/script/psi/LiteralNode.java | 45 +- .../plugin/lang/script/psi/RuleSpecNode.java | 88 ++- .../plugin/lang/script/psi/StatementNode.java | 188 +++++- .../script/psi/TopLevelStatementNode.java | 42 +- .../script/psi/VariableDeclarationNode.java | 61 +- .../script/psi/VariableDeclaratorNode.java | 68 +- .../script/psi/VariableDeclaratorsNode.java | 47 +- .../lang/script/reference/ClassReference.java | 33 + .../VariableDeclarationReference.java | 33 + .../nop/idea/plugin/utils/PsiClassHelper.java | 3 - .../plugin/lang/TestXLangScriptParser.java | 11 +- .../resources/_vfs/test/ast/statement-1.ast | 379 +++++++++--- 26 files changed, 1590 insertions(+), 503 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarDecl.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarScope.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrayExpressionNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/VariableDeclarationReference.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarDecl.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarDecl.java new file mode 100644 index 000000000..665c8f55c --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarDecl.java @@ -0,0 +1,12 @@ +package io.nop.idea.plugin.lang; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; + +/** + * 变量定义 + * + * @author flytreeleft + * @date 2025-07-04 + */ +public record XLangVarDecl(PsiClass type, PsiElement element) {} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarScope.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarScope.java new file mode 100644 index 000000000..f72dcccd8 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarScope.java @@ -0,0 +1,19 @@ +package io.nop.idea.plugin.lang; + +import java.util.Map; + +import org.jetbrains.annotations.NotNull; + +/** + * 变量作用域 + * + * @author flytreeleft + * @date 2025-07-04 + */ +public interface XLangVarScope { + + /** 获取当前作用域内所定义的变量 */ + default @NotNull Map getVars() { + return Map.of(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java index 5be34063b..801e9a2a8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java @@ -5,11 +5,11 @@ import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.tree.IElementType; import io.nop.idea.plugin.lang.script.psi.Identifier; -import io.nop.xlang.parse.antlr.XLangLexer; -import org.antlr.intellij.adaptor.lexer.TokenIElementType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_Identifier; + /** * @author flytreeleft * @date 2025-06-28 @@ -21,14 +21,11 @@ public class XLangScriptASTFactory extends ASTFactory { /** 为 AST 树的叶子节点创建 {@link com.intellij.psi.PsiElement PsiElement} */ @Override - public @Nullable LeafElement createLeaf(@NotNull IElementType type, @NotNull CharSequence text) { - if (!(type instanceof TokenIElementType token)) { - return null; + public @Nullable LeafElement createLeaf(@NotNull IElementType token, @NotNull CharSequence text) { + if (token == TOKEN_Identifier) { + return new Identifier(token, text); } - return switch (token.getANTLRTokenType()) { - case XLangLexer.Identifier -> new Identifier(token, text); - default -> new LeafPsiElement(token, text); - }; + return new LeafPsiElement(token, text); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index 97bb50e39..3dbac8b39 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -1,7 +1,5 @@ package io.nop.idea.plugin.lang.script; -import java.util.List; - import com.intellij.lang.ASTNode; import com.intellij.lang.ParserDefinition; import com.intellij.lang.PsiParser; @@ -12,8 +10,10 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.tree.IFileElementType; import com.intellij.psi.tree.TokenSet; +import io.nop.idea.plugin.lang.script.psi.ArrayExpressionNode; import io.nop.idea.plugin.lang.script.psi.ArrowFunctionBodyNode; import io.nop.idea.plugin.lang.script.psi.ArrowFunctionNode; +import io.nop.idea.plugin.lang.script.psi.AssignmentExpressionNode; import io.nop.idea.plugin.lang.script.psi.BlockStatementNode; import io.nop.idea.plugin.lang.script.psi.CalleeArgumentsNode; import io.nop.idea.plugin.lang.script.psi.ExpressionNode; @@ -36,13 +36,14 @@ import io.nop.idea.plugin.lang.script.psi.TopLevelStatementNode; import io.nop.idea.plugin.lang.script.psi.VariableDeclarationNode; import io.nop.idea.plugin.lang.script.psi.VariableDeclaratorNode; import io.nop.idea.plugin.lang.script.psi.VariableDeclaratorsNode; -import io.nop.xlang.parse.antlr.XLangLexer; import io.nop.xlang.parse.antlr.XLangParser; -import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; import org.antlr.intellij.adaptor.lexer.RuleIElementType; -import org.antlr.intellij.adaptor.lexer.TokenIElementType; import org.jetbrains.annotations.NotNull; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_comment; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_string; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_whitespace; + /** * 参考 https://github.com/antlr/antlr4-intellij-adaptor/blob/master/src/test/java/issue2/Issue2ParserDefinition.java * @@ -52,30 +53,6 @@ import org.jetbrains.annotations.NotNull; public class XLangScriptParserDefinition implements ParserDefinition { public static final IFileElementType FILE = new IFileElementType(XLangScriptLanguage.INSTANCE); - public static TokenIElementType ID; - - static { - PSIElementTypeFactory.defineLanguageIElementTypes(XLangScriptLanguage.INSTANCE, - XLangLexer.tokenNames, - XLangParser.ruleNames); - List tokenIElementTypes - = PSIElementTypeFactory.getTokenIElementTypes(XLangScriptLanguage.INSTANCE); - - ID = tokenIElementTypes.get(XLangLexer.Identifier); - } - - public static final TokenSet COMMENTS = PSIElementTypeFactory.createTokenSet(XLangScriptLanguage.INSTANCE, - XLangLexer.SingleLineComment, - XLangLexer.MultiLineComment); - - public static final TokenSet WHITESPACE = PSIElementTypeFactory.createTokenSet(XLangScriptLanguage.INSTANCE, - XLangLexer.WhiteSpaces, - XLangLexer.LineTerminator); - - public static final TokenSet STRING = PSIElementTypeFactory.createTokenSet(XLangScriptLanguage.INSTANCE, - XLangLexer.StringLiteral, - XLangLexer.TemplateStringLiteral); - @NotNull @Override public Lexer createLexer(Project project) { @@ -121,6 +98,10 @@ public class XLangScriptParserDefinition implements ParserDefinition { new VariableDeclaratorNode(node); case XLangParser.RULE_blockStatement -> // new BlockStatementNode(node); + case XLangParser.RULE_assignmentExpression -> // + new AssignmentExpressionNode(node); + case XLangParser.RULE_arrayExpression -> // + new ArrayExpressionNode(node); // case XLangParser.RULE_expression_single -> // new ExpressionNode(node); @@ -172,18 +153,18 @@ public class XLangScriptParserDefinition implements ParserDefinition { @NotNull @Override public TokenSet getWhitespaceTokens() { - return WHITESPACE; + return TOKEN_whitespace; } @NotNull @Override public TokenSet getCommentTokens() { - return COMMENTS; + return TOKEN_comment; } @NotNull @Override public TokenSet getStringLiteralElements() { - return STRING; + return TOKEN_literal_string; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java new file mode 100644 index 000000000..d717c2af2 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java @@ -0,0 +1,59 @@ +package io.nop.idea.plugin.lang.script; + +import java.util.List; + +import com.intellij.psi.tree.TokenSet; +import io.nop.xlang.parse.antlr.XLangLexer; +import io.nop.xlang.parse.antlr.XLangParser; +import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; +import org.antlr.intellij.adaptor.lexer.RuleIElementType; +import org.antlr.intellij.adaptor.lexer.TokenIElementType; + +/** + * @author flytreeleft + * @date 2025-07-04 + */ +public class XLangScriptTokenTypes { + static { + PSIElementTypeFactory.defineLanguageIElementTypes(XLangScriptLanguage.INSTANCE, + XLangLexer.tokenNames, + XLangParser.ruleNames); + } + + public static final List TOKEN_ELEMENT_TYPES = // + PSIElementTypeFactory.getTokenIElementTypes(XLangScriptLanguage.INSTANCE); + public static final List RULE_ELEMENT_TYPES = // + PSIElementTypeFactory.getRuleIElementTypes(XLangScriptLanguage.INSTANCE); + + public static final TokenIElementType TOKEN_Identifier = token(XLangLexer.Identifier); + + public static final TokenSet TOKEN_comment = tokenSet(XLangLexer.SingleLineComment, XLangLexer.MultiLineComment); + public static final TokenSet TOKEN_whitespace = tokenSet(XLangLexer.WhiteSpaces, XLangLexer.LineTerminator); + + public static final TokenSet TOKEN_literal_string = tokenSet(XLangLexer.StringLiteral, + XLangLexer.TemplateStringLiteral); + + public static final TokenIElementType TOKEN_literal_boolean = token(XLangLexer.BooleanLiteral); + public static final TokenIElementType TOKEN_literal_decimal = token(XLangLexer.DecimalLiteral); + public static final TokenIElementType TOKEN_literal_regex = token(XLangLexer.RegularExpressionLiteral); + public static final TokenSet TOKEN_literal_integer = tokenSet(XLangLexer.BinaryIntegerLiteral, + XLangLexer.DecimalIntegerLiteral, + XLangLexer.HexIntegerLiteral); + + public static final RuleIElementType RULE_ast_identifierOrPattern = rule(XLangParser.RULE_ast_identifierOrPattern); + public static final RuleIElementType RULE_expression_initializer = rule(XLangParser.RULE_expression_initializer); + public static final RuleIElementType RULE_moduleDeclaration_import + = rule(XLangParser.RULE_moduleDeclaration_import); + + public static TokenSet tokenSet(int... types) { + return PSIElementTypeFactory.createTokenSet(XLangScriptLanguage.INSTANCE, types); + } + + public static TokenIElementType token(int type) { + return TOKEN_ELEMENT_TYPES.get(type); + } + + public static RuleIElementType rule(int type) { + return RULE_ELEMENT_TYPES.get(type); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java index 0a14f5f11..852e44739 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/completion/XLangScriptCompletionProvider.java @@ -71,7 +71,7 @@ public class XLangScriptCompletionProvider extends CompletionProvider[a, b, c]: + *

+ * RuleSpecNode(arrayExpression)
+ *   PsiElement('[')('[')
+ *   RuleSpecNode(elementList_)
+ *     RuleSpecNode(ast_arrayElement)
+ *       ExpressionNode(expression_single)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('a')
+ *     PsiElement(',')(',')
+ *     PsiWhiteSpace(' ')
+ *     RuleSpecNode(ast_arrayElement)
+ *       ExpressionNode(expression_single)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('b')
+ *     PsiElement(',')(',')
+ *     PsiWhiteSpace(' ')
+ *     RuleSpecNode(ast_arrayElement)
+ *       ExpressionNode(expression_single)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('c')
+ *   PsiElement(']')(']')
+ * 
+ * + * @author flytreeleft + * @date 2025-07-04 + */ +public class ArrayExpressionNode extends RuleSpecNode { + + public ArrayExpressionNode(@NotNull ASTNode node) { + super(node); + } + + /** 获取数组元素类型 */ + public PsiClass getElementType() { + // TODO 返回第一个不为 null 的元素类型 + return null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java new file mode 100644 index 000000000..80d948ca1 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java @@ -0,0 +1,73 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import org.jetbrains.annotations.NotNull; + +/** + * 变量赋值 xyz = "234";: + *
+ * RuleSpecNode(assignmentExpression)
+ *   RuleSpecNode(expression_leftHandSide)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('xyz')
+ *   PsiWhiteSpace(' ')
+ *   RuleSpecNode(assignmentOperator_)
+ *     PsiElement('=')('=')
+ *   PsiWhiteSpace(' ')
+ *   ExpressionNode(expression_single)
+ *     LiteralNode(literal)
+ *       RuleSpecNode(literal_string)
+ *         PsiElement(StringLiteral)('"234"')
+ *   RuleSpecNode(eos__)
+ *     PsiElement(';')(';')
+ * 
+ * + * 数组元素赋值 arr[0] = 'a';: + *
+ * AssignmentExpressionNode(assignmentExpression)
+ *   RuleSpecNode(expression_leftHandSide)
+ *     RuleSpecNode(memberExpression)
+ *       ExpressionNode(expression_single)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('arr')
+ *       PsiElement('[')('[')
+ *       ExpressionNode(expression_single)
+ *         LiteralNode(literal)
+ *           RuleSpecNode(literal_numeric)
+ *             PsiElement(DecimalIntegerLiteral)('0')
+ *       PsiElement(']')(']')
+ *   PsiWhiteSpace(' ')
+ *   RuleSpecNode(assignmentOperator_)
+ *     PsiElement('=')('=')
+ *   PsiWhiteSpace(' ')
+ *   ExpressionNode(expression_single)
+ *     LiteralNode(literal)
+ *       RuleSpecNode(literal_string)
+ *         PsiElement(StringLiteral)(''a'')
+ *   RuleSpecNode(eos__)
+ *     PsiElement(';')(';')
+ * 
+ * + * @author flytreeleft + * @date 2025-07-04 + */ +public class AssignmentExpressionNode extends RuleSpecNode { + private ExpressionNode expression; + + public AssignmentExpressionNode(@NotNull ASTNode node) { + super(node); + } + + public IdentifierNode getVarNameNode() { + RuleSpecNode node = (RuleSpecNode) getFirstChild().getFirstChild(); + return node instanceof IdentifierNode ? (IdentifierNode) node : null; + } + + public PsiClass getVarType() { + if (expression == null || !expression.isValid()) { + expression = findChildByClass(ExpressionNode.class); + } + return expression != null ? expression.getResultType() : null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java index aef25e42f..168b3adae 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java @@ -1,10 +1,28 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; import org.jetbrains.annotations.NotNull; /** * 函数调用的参数列表节点 + *

+ * 参数列表 (1, 2): + *

+ * CalleeArgumentsNode(arguments_)
+ *   PsiElement('(')('(')
+ *   ExpressionNode(expression_single)
+ *     LiteralNode(literal)
+ *       RuleSpecNode(literal_numeric)
+ *         PsiElement(DecimalIntegerLiteral)('1')
+ *   PsiElement(',')(',')
+ *   PsiWhiteSpace(' ')
+ *   ExpressionNode(expression_single)
+ *     LiteralNode(literal)
+ *       RuleSpecNode(literal_numeric)
+ *         PsiElement(DecimalIntegerLiteral)('2')
+ *   PsiElement(')')(')')
+ * 
* * @author flytreeleft * @date 2025-06-30 @@ -14,4 +32,16 @@ public class CalleeArgumentsNode extends RuleSpecNode { public CalleeArgumentsNode(@NotNull ASTNode node) { super(node); } + + public PsiClass @NotNull [] getArgumentTypes() { + ExpressionNode[] exprs = findChildrenByClass(ExpressionNode.class); + + PsiClass[] types = new PsiClass[exprs.length]; + for (int i = 0; i < exprs.length; i++) { + ExpressionNode expr = exprs[i]; + + types[i] = expr.getResultType(); + } + return types; + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index 3c9ebf6af..59aeb4b2d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -1,6 +1,5 @@ package io.nop.idea.plugin.lang.script.psi; -import java.util.Collection; import java.util.function.BiFunction; import com.intellij.lang.ASTNode; @@ -9,13 +8,13 @@ import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiField; import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiType; import com.intellij.psi.impl.PsiClassImplUtil; import com.intellij.psi.util.PsiTreeUtil; import io.nop.idea.plugin.lang.script.reference.ClassMethodReference; import io.nop.idea.plugin.lang.script.reference.ClassPropertyReference; -import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; /** @@ -36,20 +35,283 @@ import org.jetbrains.annotations.NotNull; * - 2 * - 3 * + *

+ * 对象方法调用 a.b(c, d): + *

+ * ExpressionNode(expression_single)
+ *   ExpressionNode(expression_single)
+ *     ExpressionNode(expression_single)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('a')
+ *     PsiElement('.')('.')
+ *     ObjectMemberNode(identifier_ex)
+ *       RuleSpecNode(identifierOrKeyword_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('b')
+ *   CalleeArgumentsNode(arguments_)
+ *     PsiElement('(')('(')
+ *     ExpressionNode(expression_single)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('c')
+ *     PsiElement(',')(',')
+ *     PsiWhiteSpace(' ')
+ *     ExpressionNode(expression_single)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('d')
+ *     PsiElement(')')(')')
+ * 
+ * + * 函数调用 a(1, 2): + *
+ * ExpressionNode(expression_single)
+ *   ExpressionNode(expression_single)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('a')
+ *   CalleeArgumentsNode(arguments_)
+ *     PsiElement('(')('(')
+ *     ExpressionNode(expression_single)
+ *       LiteralNode(literal)
+ *         RuleSpecNode(literal_numeric)
+ *           PsiElement(DecimalIntegerLiteral)('1')
+ *     PsiElement(',')(',')
+ *     PsiWhiteSpace(' ')
+ *     ExpressionNode(expression_single)
+ *       LiteralNode(literal)
+ *         RuleSpecNode(literal_numeric)
+ *           PsiElement(DecimalIntegerLiteral)('2')
+ *     PsiElement(')')(')')
+ * 
+ * + * 构造函数调用 new String("abc"): + *
+ * ExpressionNode(expression_single)
+ *   PsiElement('new')('new')
+ *   PsiWhiteSpace(' ')
+ *   ParameterizedTypeNode(parameterizedTypeNode)
+ *     RuleSpecNode(qualifiedName_)
+ *       RuleSpecNode(qualifiedName)
+ *         RuleSpecNode(qualifiedName_name_)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('String')
+ *   CalleeArgumentsNode(arguments_)
+ *     PsiElement('(')('(')
+ *     ExpressionNode(expression_single)
+ *       LiteralNode(literal)
+ *         RuleSpecNode(literal_string)
+ *           PsiElement(StringLiteral)('"abc"')
+ *     PsiElement(')')(')')
+ * 
+ * + * 访问对象的成员变量 a.b.c: + *
+ * ExpressionNode(expression_single)
+ *   ExpressionNode(expression_single)
+ *     ExpressionNode(expression_single)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('a')
+ *     PsiElement('.')('.')
+ *     ObjectMemberNode(identifier_ex)
+ *       RuleSpecNode(identifierOrKeyword_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('b')
+ *   PsiElement('.')('.')
+ *   ObjectMemberNode(identifier_ex)
+ *     RuleSpecNode(identifierOrKeyword_)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('c')
+ * 
+ * + * 对象声明 {a, b: 1}: + *
+ * ExpressionNode(expression_single)
+ *   ObjectDeclarationNode(objectExpression)
+ *     PsiElement('{')('{')
+ *     RuleSpecNode(objectProperties_)
+ *       ObjectPropertyDeclarationNode(ast_objectProperty)
+ *         ObjectPropertyAssignmentNode(propertyAssignment)
+ *           ObjectMemberNode(identifier_ex)
+ *             RuleSpecNode(identifierOrKeyword_)
+ *               IdentifierNode(identifier)
+ *                 PsiElement(Identifier)('a')
+ *       PsiElement(',')(',')
+ *       PsiWhiteSpace(' ')
+ *       ObjectPropertyDeclarationNode(ast_objectProperty)
+ *         ObjectPropertyAssignmentNode(propertyAssignment)
+ *           RuleSpecNode(expression_propName)
+ *             ObjectMemberNode(identifier_ex)
+ *               RuleSpecNode(identifierOrKeyword_)
+ *                 IdentifierNode(identifier)
+ *                   PsiElement(Identifier)('b')
+ *           PsiElement(':')(':')
+ *           PsiWhiteSpace(' ')
+ *           ExpressionNode(expression_single)
+ *             LiteralNode(literal)
+ *               RuleSpecNode(literal_numeric)
+ *                 PsiElement(DecimalIntegerLiteral)('1')
+ *     PsiElement('}')('}')
+ * 
+ * + * 箭头函数声明 (a, b) => a + b: + *
+ * ExpressionNode(expression_single)
+ *   ArrowFunctionNode(arrowFunctionExpression)
+ *     PsiElement('(')('(')
+ *     RuleSpecNode(parameterList_)
+ *       FunctionParameterDeclarationNode(parameterDeclaration)
+ *         RuleSpecNode(ast_identifierOrPattern)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('a')
+ *       PsiElement(',')(',')
+ *       PsiWhiteSpace(' ')
+ *       FunctionParameterDeclarationNode(parameterDeclaration)
+ *         RuleSpecNode(ast_identifierOrPattern)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('b')
+ *     PsiElement(')')(')')
+ *     PsiWhiteSpace(' ')
+ *     PsiElement('=>')('=>')
+ *     PsiWhiteSpace(' ')
+ *     ArrowFunctionBodyNode(expression_functionBody)
+ *       ExpressionNode(expression_single)
+ *         ExpressionNode(expression_single)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('a')
+ *         PsiWhiteSpace(' ')
+ *         PsiElement('+')('+')
+ *         PsiWhiteSpace(' ')
+ *         ExpressionNode(expression_single)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('b')
+ * 
+ * + * 变量运算 a + b: + *
+ * ExpressionNode(expression_single)
+ *   ExpressionNode(expression_single)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('a')
+ *   PsiWhiteSpace(' ')
+ *   PsiElement('+')('+')
+ *   PsiWhiteSpace(' ')
+ *   ExpressionNode(expression_single)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('b')
+ * 
+ * + * 变量运算 a > 2: + *
+ * ExpressionNode(expression_single)
+ *   ExpressionNode(expression_single)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('a')
+ *   PsiWhiteSpace(' ')
+ *   PsiElement('>')('>')
+ *   PsiWhiteSpace(' ')
+ *   ExpressionNode(expression_single)
+ *     LiteralNode(literal)
+ *       RuleSpecNode(literal_numeric)
+ *         PsiElement(DecimalIntegerLiteral)('2')
+ * 
+ * + * 构造函数及其方法的调用 new String("def").trim(): + *
+ * ExpressionNode(expression_single)
+ *   ExpressionNode(expression_single)
+ *     ExpressionNode(expression_single)
+ *       PsiElement('new')('new')
+ *       PsiWhiteSpace(' ')
+ *       ParameterizedTypeNode(parameterizedTypeNode)
+ *         RuleSpecNode(qualifiedName_)
+ *           RuleSpecNode(qualifiedName)
+ *             RuleSpecNode(qualifiedName_name_)
+ *               IdentifierNode(identifier)
+ *                 PsiElement(Identifier)('String')
+ *       CalleeArgumentsNode(arguments_)
+ *         PsiElement('(')('(')
+ *         ExpressionNode(expression_single)
+ *           LiteralNode(literal)
+ *             RuleSpecNode(literal_string)
+ *               PsiElement(StringLiteral)('"def"')
+ *         PsiElement(')')(')')
+ *     PsiElement('.')('.')
+ *     ObjectMemberNode(identifier_ex)
+ *       RuleSpecNode(identifierOrKeyword_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('trim')
+ *   CalleeArgumentsNode(arguments_)
+ *     PsiElement('(')('(')
+ *     PsiElement(')')(')')
+ * 
+ * + * 数组声明 [a, b, c]: + *
+ * ExpressionNode(expression_single)
+ *   RuleSpecNode(arrayExpression)
+ *     PsiElement('[')('[')
+ *     RuleSpecNode(elementList_)
+ *       RuleSpecNode(ast_arrayElement)
+ *         ExpressionNode(expression_single)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('a')
+ *       PsiElement(',')(',')
+ *       PsiWhiteSpace(' ')
+ *       RuleSpecNode(ast_arrayElement)
+ *         ExpressionNode(expression_single)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('b')
+ *       PsiElement(',')(',')
+ *       PsiWhiteSpace(' ')
+ *       RuleSpecNode(ast_arrayElement)
+ *         ExpressionNode(expression_single)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('c')
+ *     PsiElement(']')(']')
+ * 
* * @author flytreeleft * @date 2025-06-30 */ public class ExpressionNode extends RuleSpecNode { + private PsiClass[] resultType; public ExpressionNode(@NotNull ASTNode node) { super(node); } + /** + * 获取表达式结果的类型 + * + * @return null 为有效值 + */ + public PsiClass getResultType() { + if (resultType == null) { + resultType = new PsiClass[] { doGetResultType() }; + } + return resultType[0]; + } + @Override public PsiReference @NotNull [] doGetReferences() { + // Note: 仅识别当前表达式的最后一个有效元素的引用,其余部分,由其子表达式做识别处理 + + PsiElement firstChild = getFirstChild(); + // 变量引用:abc + if (firstChild instanceof IdentifierNode i) { + TextRange textRange = i.getTextRangeInParent(); + + return i.createReferences(this, textRange); + } + // 构造函数:new String("abc") + else if (isObjectConstructorCall()) { + ParameterizedTypeNode cons = PsiTreeUtil.findChildOfType(this, ParameterizedTypeNode.class); + TextRange textRange = cons.getTextRangeInParent(); + + IdentifierNode proto = (IdentifierNode) PsiTreeUtil.getDeepestLast(cons).getParent(); + + return proto.createReferences(this, textRange); + } // 对象声明:{a, b: 1} - if (isObjectDeclaration()) { + else if (isObjectDeclaration()) { return PsiReference.EMPTY_ARRAY; } // 对象方法调用:a.b.c(1, 2) @@ -65,14 +327,10 @@ public class ExpressionNode extends RuleSpecNode { return new PsiReference[] { ref }; } } - // 函数调用:fn1(1, 2, 3) - else if (isFunctionCall()) { - // TODO 构造函数引用 - return PsiReference.EMPTY_ARRAY; - } // 对象属性访问:a.b.c - else if (isObjectMember()) { + else if (isObjectMemberAccess()) { PsiField prop = getObjectProperty(); + if (prop != null) { TextRange propTextRange = getObjectMemberTextRange(); ClassPropertyReference ref = new ClassPropertyReference(this, prop, propTextRange); @@ -80,35 +338,73 @@ public class ExpressionNode extends RuleSpecNode { return new PsiReference[] { ref }; } } + // 函数调用:fn1(1, 2, 3) + else if (isFunctionCall()) { + ExpressionNode callee = (ExpressionNode) getFirstChild(); + TextRange textRange = callee.getTextRangeInParent(); + + IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); + + return fn.createReferences(this, textRange); + } return PsiReference.EMPTY_ARRAY; } - /** 当前表达式是否为标识符 */ - public boolean isIdentifier() { - return getFirstChild() instanceof IdentifierNode; - } + protected PsiClass doGetResultType() { + PsiElement firstChild = getFirstChild(); + + if (firstChild instanceof LiteralNode l) { + return l.getDataType(); + } // + else if (firstChild instanceof IdentifierNode i) { + return i.getDataType(); + } // + else if (isObjectConstructorCall()) { + ParameterizedTypeNode cons = PsiTreeUtil.findChildOfType(this, ParameterizedTypeNode.class); + IdentifierNode proto = (IdentifierNode) PsiTreeUtil.getDeepestLast(cons).getParent(); + + return proto.getDataType(); + } // + else if (isObjectMethodCall()) { + PsiMethod method = getObjectMethod(); + PsiType returnType = method != null ? method.getReturnType() : null; + + return getPsiClassByPsiType(returnType); + } + // 若不是对象的方法调用,便是对象的属性访问 + else if (isObjectMemberAccess()) { + PsiField prop = getObjectProperty(); + PsiType propType = prop != null ? prop.getType() : null; - /** 当前表达式是否为字面量 */ - public boolean isLiteral() { - return getFirstChild() instanceof LiteralNode; + return getPsiClassByPsiType(propType); + } // + else if (isFunctionCall()) { + ExpressionNode callee = (ExpressionNode) getFirstChild(); + IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); + + // Note: 对应的是函数的返回值类型 + return fn.getDataType(); + } // + else if (isArrowFunction()) { + ArrowFunctionNode fn = (ArrowFunctionNode) getFirstChild(); + + return fn.getReturnType(); + } // + else if (isArrayInit()) { + ArrayExpressionNode array = (ArrayExpressionNode) getFirstChild(); + + return array.getElementType(); + } + + // TODO 运算表达式,如 a + b + return null; } - /** 当前表达式是否为访问对象成员(成员变量或方法) */ - public boolean isObjectMember() { - /* 对象成员访问,如 a.b: - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('b') - */ - if (getFirstChild() instanceof ExpressionNode obj) { + /** 当前表达式是否为对象成员(成员变量或方法)访问 */ + public boolean isObjectMemberAccess() { + // a.b.c + if (getFirstChild() instanceof ExpressionNode) { return getLastChild() instanceof ObjectMemberNode; } return false; @@ -122,124 +418,55 @@ public class ExpressionNode extends RuleSpecNode { * 一般不为对象声明中的属性构造引用 */ public boolean isObjectDeclaration() { - /* 对象声明,如 {a, b: 1}: - ExpressionNode(expression_single) - ObjectDeclarationNode(objectExpression) - PsiElement('{')('{') - RuleSpecNode(objectProperties_) - ObjectPropertyDeclarationNode(ast_objectProperty) - ObjectPropertyAssignmentNode(propertyAssignment) - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - PsiElement(',')(',') - ObjectPropertyDeclarationNode(ast_objectProperty) - ObjectPropertyAssignmentNode(propertyAssignment) - RuleSpecNode(expression_propName) - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('b') - PsiElement(':')(':') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('1') - PsiElement('}')('}') - */ + // {a, b: 1} return getFirstChild() instanceof ObjectDeclarationNode; } /** 当前表达式是否为对象方法调用 */ public boolean isObjectMethodCall() { - /* 对象方法调用,如 a.b(1): - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('b') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('1') - PsiElement(')')(')') - */ + // a.b.c() if (getFirstChild() instanceof ExpressionNode obj) { - return obj.isObjectMember() && getLastChild() instanceof CalleeArgumentsNode; + return obj.isObjectMemberAccess() && getLastChild() instanceof CalleeArgumentsNode; } return false; } /** 当前表达式是否为对象构造函数调用 */ public boolean isObjectConstructorCall() { - /* 对象构造方法调用,如 new String("abc"): - ExpressionNode(expression_single) - PsiElement('new')('new') - ParameterizedTypeNode(parameterizedTypeNode) - RuleSpecNode(qualifiedName_) - RuleSpecNode(qualifiedName) - RuleSpecNode(qualifiedName_name_) - IdentifierNode(identifier) - PsiElement(Identifier)('String') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_string) - PsiElement(StringLiteral)('"abc"') - PsiElement(')')(')') - */ + // new String("abc") return getFirstChild().getText().equals("new") && getLastChild() instanceof CalleeArgumentsNode; } /** 当前表达式是否为函数调用 */ public boolean isFunctionCall() { - /* 函数调用,如 a(1): - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('1') - PsiElement(')')(')') - */ + // a(1) if (getFirstChild() instanceof ExpressionNode callee) { - return callee.isIdentifier() && getLastChild() instanceof CalleeArgumentsNode; + return callee.getFirstChild() instanceof IdentifierNode // + && getLastChild() instanceof CalleeArgumentsNode; } return false; } /** 当前表达式是否为箭头函数 */ public boolean isArrowFunction() { + // (a, b) => a + b return getFirstChild() instanceof ArrowFunctionNode; } + /** 当前表达式是否为数组初始化 */ + public boolean isArrayInit() { + // [1, 2, 3] + return getFirstChild() instanceof ArrayExpressionNode; + } + /** 获取对象的方法 */ protected PsiMethod getObjectMethod() { ExpressionNode obj = (ExpressionNode) getFirstChild(); - PsiMethod[] objMethods = obj.getObjectMethods(); - PsiClass[] objMethodArgTypes = getObjectMethodArgumentTypes(); + PsiMethod[] methods = obj.getObjectMethods(); + PsiClass[] argTypes = getObjectMethodArgumentTypes(); - // Note: 只有通过参数列表才能唯一确定调用方的方法 - for (PsiMethod method : objMethods) { - if (matchMethodArgs(method, objMethodArgTypes)) { - return method; - } - } - return null; + return filterMethodByArgs(methods, argTypes); } /** 获取对象成员在当前表达式中的 {@link TextRange} */ @@ -261,98 +488,71 @@ public class ExpressionNode extends RuleSpecNode { null); } + /** 获取调用参数的类型列表 */ + protected PsiClass[] getObjectMethodArgumentTypes() { + CalleeArgumentsNode calleeArgs = (CalleeArgumentsNode) getLastChild(); + + return calleeArgs.getArgumentTypes(); + } + protected T getObjectMember(BiFunction consumer, T defaultValue) { ExpressionNode obj = (ExpressionNode) getFirstChild(); - ObjectMemberNode member = (ObjectMemberNode) getLastChild(); - PsiClass objClass = obj.getResultType(); + if (objClass == null) { return defaultValue; } + ObjectMemberNode member = (ObjectMemberNode) getLastChild(); String memberName = member.getText(); - return consumer.apply(objClass, memberName); - } - /** 获取调用参数类型列表 */ - protected PsiClass[] getObjectMethodArgumentTypes() { - CalleeArgumentsNode node = (CalleeArgumentsNode) getLastChild(); - /* - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('1') - PsiElement(',')(',') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('2') - PsiElement(')')(')') - */ - Collection argNodeList = PsiTreeUtil.findChildrenOfType(node, ExpressionNode.class); - - return argNodeList.stream().map(ExpressionNode::getResultType).toArray(PsiClass[]::new); + return consumer.apply(objClass, memberName); } - /** - * 获取表达式结果的类型 - *

- * null 为有效值,可能是字面量即为 null - */ - protected PsiClass getResultType() { - PsiElement firstChild = getFirstChild(); - - if (firstChild instanceof LiteralNode literal) { - return literal.getDataType(); - } // - else if (firstChild instanceof IdentifierNode identifier) { - return identifier.getDataType(); - } // - else if (isObjectMethodCall()) { - PsiMethod method = getObjectMethod(); - PsiType returnType = method != null ? method.getReturnType() : null; - - return PsiClassHelper.getTypeClass(getProject(), returnType); - } // - else if (isObjectMember()) { - PsiField prop = getObjectProperty(); - PsiType propType = prop != null ? prop.getType() : null; + protected PsiMethod filterMethodByArgs(PsiMethod[] methods, PsiClass[] args) { + // 只有唯一的方法,则直接返回 + if (methods.length == 1) { + return methods[0]; + } - return PsiClassHelper.getTypeClass(getProject(), propType); - } // - else if (isObjectConstructorCall()) { - ParameterizedTypeNode cst = PsiTreeUtil.findChildOfType(this, ParameterizedTypeNode.class); - IdentifierNode typeNode = (IdentifierNode) PsiTreeUtil.getDeepestLast(cst).getParent(); + // 优先查找参数列表完全匹配的方法 + for (PsiMethod method : methods) { + PsiParameter[] params = method.getParameterList().getParameters(); - return typeNode.getDataType(); - } // - else if (isFunctionCall()) { - // TODO 分析 return 表达式,得到返回类型 - return null; - } // - else if (isArrowFunction()) { - ArrowFunctionNode fn = (ArrowFunctionNode) getFirstChild(); + if (matchMethodParams(params, args)) { + return method; + } + } - return fn.getReturnType(); + // 再查找参数数量一致的方法 + for (PsiMethod method : methods) { + if (method.getParameterList().getParametersCount() == args.length) { + return method; + } } - /* TODO 复杂运算,如 a + b - RuleSpecNode(expression_single) - RuleSpecNode(expression_single) - RuleSpecNode(identifier) - PsiElement(Identifier)('a') - PsiElement('+')('+') - RuleSpecNode(expression_single) - RuleSpecNode(identifier) - PsiElement(Identifier)('b') - */ return null; } - protected boolean matchMethodArgs(PsiMethod method, PsiClass[] args) { - // TODO 依次比较方法的参数类型 - return method.getParameterList().getParametersCount() == args.length; + protected boolean matchMethodParams(PsiParameter[] params, PsiClass[] args) { + if (params.length != args.length) { + return false; + } + + for (int i = 0; i < params.length; i++) { + PsiClass arg = args[i]; + PsiClass param = getPsiClassByPsiType(params[i].getType()); + + if (arg == param) { + continue; + } + + if (arg == null || param == null // + || !arg.isInheritor(param, true) // + ) { + return false; + } + } + return true; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java index a3ec26ac9..0856c4852 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java @@ -1,17 +1,89 @@ package io.nop.idea.plugin.lang.script.psi; +import java.util.Map; + import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import io.nop.idea.plugin.lang.XLangVarDecl; import org.jetbrains.annotations.NotNull; /** * 函数声明节点 + *

+ * function fn(a, b) { return a + b; }: + *

+ * FunctionDeclarationNode(functionDeclaration)
+ *   PsiElement('function')('function')
+ *   PsiWhiteSpace(' ')
+ *   IdentifierNode(identifier)
+ *     PsiElement(Identifier)('fn')
+ *   PsiElement('(')('(')
+ *   RuleSpecNode(parameterList_)
+ *     FunctionParameterDeclarationNode(parameterDeclaration)
+ *       RuleSpecNode(ast_identifierOrPattern)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('a')
+ *     PsiElement(',')(',')
+ *     PsiWhiteSpace(' ')
+ *     FunctionParameterDeclarationNode(parameterDeclaration)
+ *       RuleSpecNode(ast_identifierOrPattern)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('b')
+ *   PsiElement(')')(')')
+ *   PsiWhiteSpace(' ')
+ *   BlockStatementNode(blockStatement)
+ *     PsiElement('{')('{')
+ *     PsiWhiteSpace(' ')
+ *     RuleSpecNode(statements_)
+ *       StatementNode(statement)
+ *         ReturnStatementNode(returnStatement)
+ *           PsiElement('return')('return')
+ *           PsiWhiteSpace(' ')
+ *           ExpressionNode(expression_single)
+ *             ExpressionNode(expression_single)
+ *               ExpressionNode(expression_single)
+ *                 IdentifierNode(identifier)
+ *                   PsiElement(Identifier)('a')
+ *               PsiWhiteSpace(' ')
+ *               PsiElement('+')('+')
+ *               PsiWhiteSpace(' ')
+ *               ExpressionNode(expression_single)
+ *                 IdentifierNode(identifier)
+ *                   PsiElement(Identifier)('b')
+ *           RuleSpecNode(eos__)
+ *             PsiElement(';')(';')
+ *     PsiWhiteSpace(' ')
+ *     PsiElement('}')('}')
+ * 
* * @author flytreeleft * @date 2025-06-30 */ public class FunctionDeclarationNode extends RuleSpecNode { + private PsiClass[] returnType; public FunctionDeclarationNode(@NotNull ASTNode node) { super(node); } + + public IdentifierNode getFunctionNameNode() { + return findChildByClass(IdentifierNode.class); + } + + /** 获取函数的返回值类型 */ + public PsiClass getReturnType() { + // TODO 分析函数的 return 表达式,得到返回类型 + if (returnType == null) { + returnType = new PsiClass[] {}; + } + return returnType[0]; + } + + /** 参数列表为函数内可访问的变量 */ + @Override + public @NotNull Map getVars() { + // TODO 分析参数列表,得到参数变量及其类型 + + return Map.of(); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java index 902ef8418..bb34714ab 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java @@ -1,30 +1,69 @@ package io.nop.idea.plugin.lang.script.psi; -import java.util.Map; - import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import io.nop.idea.plugin.lang.XLangVarDecl; +import io.nop.idea.plugin.lang.script.reference.ClassReference; +import io.nop.idea.plugin.lang.script.reference.VariableDeclarationReference; import org.jetbrains.annotations.NotNull; /** * {@link Identifier} 节点 + *

+ * 该节点 AST 树为: + *

+ * IdentifierNode(identifier)
+ *   PsiElement(Identifier)('b')
+ * 
* * @author flytreeleft * @date 2025-07-02 */ public class IdentifierNode extends RuleSpecNode { + private XLangVarDecl[] varDecl; public IdentifierNode(@NotNull ASTNode node) { super(node); } - /** 获取变量的数据类型 */ + public PsiReference @NotNull [] createReferences(PsiElement source, TextRange textRange) { + XLangVarDecl varDecl = getVarDecl(); + PsiElement element = varDecl.element(); + + if (element instanceof IdentifierNode id) { + VariableDeclarationReference ref = new VariableDeclarationReference(source, id, textRange); + + return new PsiReference[] { ref }; + } else if (element instanceof PsiClass clazz) { + ClassReference ref = new ClassReference(source, clazz, textRange); + + return new PsiReference[] { ref }; + } + return PsiReference.EMPTY_ARRAY; + } + + /** + * 获取变量的数据类型 + *

+ * 若标识符为函数名,则返回函数的返回值类型 + */ public PsiClass getDataType() { - String varName = getText(); + XLangVarDecl varDecl = getVarDecl(); + + return varDecl != null ? varDecl.type() : null; + } - Map vars = getVisibleVarTypes(); - VarDecl varDecl = vars.get(varName); + public XLangVarDecl getVarDecl() { + if (varDecl == null) { + String varName = getText(); - return varDecl != null ? varDecl.type : null; + varDecl = new XLangVarDecl[] { + findVisibleVar(varName) + }; + } + return varDecl[0]; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java index 7b3b9c794..be97bc162 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java @@ -4,51 +4,65 @@ import java.util.Map; import com.intellij.lang.ASTNode; import com.intellij.psi.PsiClass; -import com.intellij.psi.util.PsiTreeUtil; +import io.nop.idea.plugin.lang.XLangVarDecl; import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; /** - * import xxx; 节点(含结束符) + * import xxx;: + *

+ * ImportDeclarationNode(importAsDeclaration)
+ *   PsiElement('import')('import')
+ *   ImportSourceNode(ast_importSource)
+ *     RuleSpecNode(qualifiedName)
+ *       RuleSpecNode(qualifiedName_name_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('java')
+ *       PsiElement('.')('.')
+ *       RuleSpecNode(qualifiedName)
+ *         RuleSpecNode(qualifiedName_name_)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('lang')
+ *         PsiElement('.')('.')
+ *         RuleSpecNode(qualifiedName)
+ *           RuleSpecNode(qualifiedName_name_)
+ *             IdentifierNode(identifier)
+ *               PsiElement(Identifier)('String')
+ *   RuleSpecNode(eos__)
+ *     PsiElement(';')(';')
+ * 
* * @author flytreeleft * @date 2025-06-29 */ public class ImportDeclarationNode extends RuleSpecNode { + private PsiClass[] clazz; public ImportDeclarationNode(@NotNull ASTNode node) { super(node); } @Override - public @NotNull Map getVarTypes() { - /* 导入语句:import java.lang.Number; - ImportDeclarationNode(importAsDeclaration) - PsiElement('import')('import') - ImportSourceNode(ast_importSource) - RuleSpecNode(qualifiedName) - RuleSpecNode(qualifiedName_name_) - IdentifierNode(identifier) - PsiElement(Identifier)('java') - PsiElement('.')('.') - RuleSpecNode(qualifiedName) - RuleSpecNode(qualifiedName_name_) - IdentifierNode(identifier) - PsiElement(Identifier)('lang') - PsiElement('.')('.') - RuleSpecNode(qualifiedName) - RuleSpecNode(qualifiedName_name_) - IdentifierNode(identifier) - PsiElement(Identifier)('Number') - RuleSpecNode(eos__) - PsiElement(';')(';') - */ - ImportSourceNode imp = PsiTreeUtil.findChildOfType(this, ImportSourceNode.class); - - String clsName = imp.getClassName(); - String clsFqn = imp.getClassFullyQualifiedName(); - PsiClass cls = PsiClassHelper.findClass(getProject(), clsFqn); - - return Map.of(clsName, new VarDecl(cls, cls)); + public @NotNull Map getVars() { + ImportSourceNode imp = findChildByClass(ImportSourceNode.class); + if (imp == null) { + return Map.of(); + } + + if (clazz == null) { + String classFQN = imp.getFullyQualifiedName(); + + clazz = new PsiClass[] { + PsiClassHelper.findClass(getProject(), classFQN) + }; + } + if (clazz[0] == null) { + return Map.of(); + } + + String varName = imp.getLastQualifiedName(); + XLangVarDecl varDecl = new XLangVarDecl(clazz[0], clazz[0]); + + return Map.of(varName, varDecl); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java index 8de9994a1..d9c7fd0c0 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -1,6 +1,7 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceProvider; import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceSet; @@ -25,18 +26,20 @@ public class ImportSourceNode extends RuleSpecNode { super(node); } - public String getClassName() { - return PsiTreeUtil.getDeepestLast(this).getText(); + public String getLastQualifiedName() { + PsiElement last = PsiTreeUtil.getDeepestLast(this); + + return last.getText(); } - public String getClassFullyQualifiedName() { + public String getFullyQualifiedName() { return getText(); } /** 构造 Java 相关的引用对象,从而支持自动补全、引用跳转、文档显示等 */ @Override protected PsiReference @NotNull [] doGetReferences() { - String fqn = getText(); + String fqn = getFullyQualifiedName(); JavaClassReferenceSet refSet = new JavaClassReferenceSet(fqn, this, 0, false, provider); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java index e04433655..5c53ff46e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java @@ -1,49 +1,50 @@ package io.nop.idea.plugin.lang.script.psi; -import java.util.regex.Pattern; - import com.intellij.lang.ASTNode; import com.intellij.psi.PsiClass; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.util.PsiTreeUtil; -import io.nop.idea.plugin.utils.PsiClassHelper; -import io.nop.xlang.parse.antlr.XLangLexer; import org.antlr.intellij.adaptor.lexer.TokenIElementType; import org.jetbrains.annotations.NotNull; /** * 字面量节点 + *

+ * 该节点 AST 树为: + *

+ * LiteralNode(literal)
+ *   RuleSpecNode(literal_numeric)
+ *     PsiElement(DecimalIntegerLiteral)('3')
+ * 
* * @author flytreeleft * @date 2025-07-02 */ public class LiteralNode extends RuleSpecNode { + private LeafPsiElement literal; + private PsiClass[] dataType; public LiteralNode(@NotNull ASTNode node) { super(node); } + public LeafPsiElement getLiteral() { + if (literal == null || !literal.isValid()) { + literal = (LeafPsiElement) PsiTreeUtil.getDeepestLast(this); + dataType = null; + } + return literal; + } + /** 获取字面量的数据类型 */ public PsiClass getDataType() { - LeafPsiElement target = (LeafPsiElement) PsiTreeUtil.getDeepestLast(this); + LeafPsiElement target = getLiteral(); - String typeName = switch (((TokenIElementType) target.getElementType()).getANTLRTokenType()) { - case XLangLexer.NullLiteral // - -> null; - case XLangLexer.BooleanLiteral // - -> Boolean.class.getName(); - case XLangLexer.DecimalLiteral // - -> Float.class.getName(); - case XLangLexer.BinaryIntegerLiteral, XLangLexer.DecimalIntegerLiteral, // - XLangLexer.HexIntegerLiteral // - -> Integer.class.getName(); - case XLangLexer.StringLiteral, XLangLexer.TemplateStringLiteral // - -> String.class.getName(); - case XLangLexer.RegularExpressionLiteral // - -> Pattern.class.getName(); - default -> null; - }; + if (dataType == null) { + TokenIElementType token = (TokenIElementType) target.getElementType(); - return typeName != null ? PsiClassHelper.findClass(getProject(), typeName) : null; + dataType = new PsiClass[] { getPsiClassByToken(token) }; + } + return dataType[0]; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java index 66fa49e35..fa6e92997 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java @@ -1,7 +1,6 @@ package io.nop.idea.plugin.lang.script.psi; -import java.util.HashMap; -import java.util.Map; +import java.util.regex.Pattern; import com.intellij.extapi.psi.ASTWrapperPsiElement; import com.intellij.lang.ASTNode; @@ -9,15 +8,27 @@ import com.intellij.openapi.application.ReadAction; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiType; +import io.nop.idea.plugin.lang.XLangVarDecl; +import io.nop.idea.plugin.lang.XLangVarScope; +import io.nop.idea.plugin.utils.PsiClassHelper; import org.antlr.intellij.adaptor.lexer.RuleIElementType; +import org.antlr.intellij.adaptor.lexer.TokenIElementType; import org.antlr.intellij.adaptor.psi.Trees; import org.jetbrains.annotations.NotNull; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_boolean; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_decimal; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_integer; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_regex; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_string; + /** * @author flytreeleft * @date 2025-06-29 */ -public class RuleSpecNode extends ASTWrapperPsiElement { +public class RuleSpecNode extends ASTWrapperPsiElement implements XLangVarScope { + private PsiReference[] refs; public RuleSpecNode(@NotNull ASTNode node) { super(node); @@ -31,23 +42,25 @@ public class RuleSpecNode extends ASTWrapperPsiElement { @Override public PsiReference @NotNull [] getReferences() { - // 在没有写入动作时,才执行函数并返回结果,从而避免阻塞编辑操作 - return ReadAction.compute(this::doGetReferences); + if (refs == null) { + // 在没有写入动作时,才执行函数并返回结果,从而避免阻塞编辑操作 + refs = ReadAction.compute(this::doGetReferences); + } + return refs; } - public IdentifierNode getIdentifier() { - return findChildByClass(IdentifierNode.class); + protected PsiReference @NotNull [] doGetReferences() { + return PsiReference.EMPTY_ARRAY; } - public boolean isRuleNode(int ruleIndex) { - return ((RuleIElementType) getNode().getElementType()).getRuleIndex() == ruleIndex; + public boolean isRuleType(RuleIElementType type) { + return getNode().getElementType() == type; } - /** 获取当前节点可访问到的变量及其类型 */ - public @NotNull Map getVisibleVarTypes() { - Map types = new HashMap<>(); - + /** 获取在当前节点上可见的指定变量 */ + public XLangVarDecl findVisibleVar(String varName) { PsiElement node = this; + while (node instanceof RuleSpecNode) { PsiElement parent = node.getParent(); @@ -57,36 +70,47 @@ public class RuleSpecNode extends ASTWrapperPsiElement { if (child == node) { break; // 只取当前节点之前定义的变量 } + if (!(child instanceof XLangVarScope scope)) { + continue; + } - if (child instanceof RuleSpecNode c) { - c.getVarTypes().forEach(types::putIfAbsent); + XLangVarDecl var = scope.getVars().get(varName); + if (var != null) { + return var; } } - } else { - // TODO 从所在的 标签中获取 xlib 函数的参数列表以及内置变量列表 + } + // 从所在的 标签中获取 xlib 函数的参数列表以及内置变量列表 + else if (parent instanceof XLangVarScope scope) { + XLangVarDecl var = scope.getVars().get(varName); + if (var != null) { + return var; + } } node = parent; } - return types; + return null; } - /** 获取当前节点所定义的变量及其类型 */ - public @NotNull Map getVarTypes() { - return Map.of(); + protected PsiClass getPsiClassByPsiType(PsiType type) { + return PsiClassHelper.getTypeClass(getProject(), type); } - protected PsiReference @NotNull [] doGetReferences() { - return PsiReference.EMPTY_ARRAY; - } - - public static class VarDecl { - public final PsiElement element; - public final PsiClass type; - - VarDecl(PsiElement element, PsiClass type) { - this.element = element; - this.type = type; + protected PsiClass getPsiClassByToken(TokenIElementType token) { + Class clazz = null; + + if (token == TOKEN_literal_boolean) { + clazz = Boolean.class; + } else if (token == TOKEN_literal_decimal) { + clazz = Float.class; + } else if (token == TOKEN_literal_regex) { + clazz = Pattern.class; + } else if (TOKEN_literal_integer.contains(token)) { + clazz = Integer.class; + } else if (TOKEN_literal_string.contains(token)) { + clazz = String.class; } + return clazz != null ? PsiClassHelper.findClass(getProject(), clazz.getName()) : null; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java index 5cb078ddf..de804d1dd 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java @@ -3,45 +3,187 @@ package io.nop.idea.plugin.lang.script.psi; import java.util.Map; import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; +import io.nop.idea.plugin.lang.XLangVarDecl; import org.jetbrains.annotations.NotNull; /** + * 赋值、声明、调用、if/switch 等语句 + *

+ * 定义变量 let b = 3;: + *

+ * StatementNode(statement)
+ *   VariableDeclarationNode(variableDeclaration)
+ *     RuleSpecNode(varModifier_)
+ *       PsiElement('let')('let')
+ *     VariableDeclaratorsNode(variableDeclarators_)
+ *       VariableDeclaratorNode(variableDeclarator)
+ *         RuleSpecNode(ast_identifierOrPattern)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('b')
+ *         RuleSpecNode(expression_initializer)
+ *           PsiElement('=')('=')
+ *           ExpressionNode(expression_single)
+ *             LiteralNode(literal)
+ *               RuleSpecNode(literal_numeric)
+ *                 PsiElement(DecimalIntegerLiteral)('3')
+ *     RuleSpecNode(eos__)
+ *       PsiElement(';')(';')
+ * 
+ * + * 函数调用 a(1, 2);: + *
+ * StatementNode(statement)
+ *   RuleSpecNode(expressionStatement)
+ *     ExpressionNode(expression_single)
+ *       ExpressionNode(expression_single)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('a')
+ *       CalleeArgumentsNode(arguments_)
+ *         PsiElement('(')('(')
+ *         ExpressionNode(expression_single)
+ *           LiteralNode(literal)
+ *             RuleSpecNode(literal_numeric)
+ *               PsiElement(DecimalIntegerLiteral)('1')
+ *         PsiElement(',')(',')
+ *         ExpressionNode(expression_single)
+ *           LiteralNode(literal)
+ *             RuleSpecNode(literal_numeric)
+ *               PsiElement(DecimalIntegerLiteral)('2')
+ *         PsiElement(')')(')')
+ *     RuleSpecNode(eos__)
+ *       PsiElement(';')(';')
+ * 
+ * + * 返回语句 return a + b + c;: + *
+ * StatementNode(statement)
+ *   ReturnStatementNode(returnStatement)
+ *     PsiElement('return')('return')
+ *     ExpressionNode(expression_single)
+ *       ExpressionNode(expression_single)
+ *         ExpressionNode(expression_single)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('a')
+ *         PsiElement('+')('+')
+ *         ExpressionNode(expression_single)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('b')
+ *       PsiElement('+')('+')
+ *       ExpressionNode(expression_single)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('c')
+ *     RuleSpecNode(eos__)
+ *       PsiElement(';')(';')
+ * 
+ * + * 函数定义 function fn2(a, b) { return a + b; }: + *
+ * StatementNode(statement)
+ *   FunctionDeclarationNode(functionDeclaration)
+ *     PsiElement('function')('function')
+ *     PsiWhiteSpace(' ')
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('fn2')
+ *     PsiElement('(')('(')
+ *     RuleSpecNode(parameterList_)
+ *       FunctionParameterDeclarationNode(parameterDeclaration)
+ *         RuleSpecNode(ast_identifierOrPattern)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('a')
+ *       PsiElement(',')(',')
+ *       PsiWhiteSpace(' ')
+ *       FunctionParameterDeclarationNode(parameterDeclaration)
+ *         RuleSpecNode(ast_identifierOrPattern)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('b')
+ *     PsiElement(')')(')')
+ *     PsiWhiteSpace(' ')
+ *     BlockStatementNode(blockStatement)
+ *       PsiElement('{')('{')
+ *       PsiWhiteSpace(' ')
+ *       RuleSpecNode(statements_)
+ *         StatementNode(statement)
+ *           ReturnStatementNode(returnStatement)
+ *             PsiElement('return')('return')
+ *             PsiWhiteSpace(' ')
+ *             ExpressionNode(expression_single)
+ *               ExpressionNode(expression_single)
+ *                 ExpressionNode(expression_single)
+ *                   IdentifierNode(identifier)
+ *                     PsiElement(Identifier)('a')
+ *                 PsiWhiteSpace(' ')
+ *                 PsiElement('+')('+')
+ *                 PsiWhiteSpace(' ')
+ *                 ExpressionNode(expression_single)
+ *                   IdentifierNode(identifier)
+ *                     PsiElement(Identifier)('b')
+ *             RuleSpecNode(eos__)
+ *               PsiElement(';')(';')
+ *       PsiWhiteSpace(' ')
+ *       PsiElement('}')('}')
+ * 
+ * + * 赋值语句 xyz = "234";: + *
+ * StatementNode(statement)
+ *   RuleSpecNode(assignmentExpression)
+ *     RuleSpecNode(expression_leftHandSide)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('xyz')
+ *     PsiWhiteSpace(' ')
+ *     RuleSpecNode(assignmentOperator_)
+ *       PsiElement('=')('=')
+ *     PsiWhiteSpace(' ')
+ *     ExpressionNode(expression_single)
+ *       LiteralNode(literal)
+ *         RuleSpecNode(literal_string)
+ *           PsiElement(StringLiteral)('"234"')
+ *     RuleSpecNode(eos__)
+ *       PsiElement(';')(';')
+ * 
+ * * @author flytreeleft * @date 2025-07-03 */ public class StatementNode extends RuleSpecNode { + private VariableDeclarationNode[] varDeclaration; public StatementNode(@NotNull ASTNode node) { super(node); } + /** 声明的变量,或者函数及其返回值类型,均为可访问变量 */ @Override - public @NotNull Map getVarTypes() { - /* 变量声明:let def = 123; - StatementNode(statement) - VariableDeclarationNode(variableDeclaration) - RuleSpecNode(varModifier_) - PsiElement('let')('let') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) - RuleSpecNode(ast_identifierOrPattern) - IdentifierNode(identifier) - PsiElement(Identifier)('def') - RuleSpecNode(expression_initializer) - PsiElement('=')('=') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('123') - RuleSpecNode(eos__) - PsiElement(';')(';') - */ - for (PsiElement child : getChildren()) { - if (child instanceof VariableDeclarationNode declaration) { - return declaration.getVarTypes(); + public @NotNull Map getVars() { + PsiElement firstChild = getFirstChild(); + + if (firstChild instanceof VariableDeclarationNode varDecl) { + return varDecl.getVars(); + } // + else if (firstChild instanceof FunctionDeclarationNode fnDecl) { + IdentifierNode varNameNode = fnDecl.getFunctionNameNode(); + String varName = varNameNode.getText(); + PsiClass varType = fnDecl.getReturnType(); + + XLangVarDecl varDecl = new XLangVarDecl(varType, varNameNode); + + return Map.of(varName, varDecl); + } // + else if (firstChild instanceof AssignmentExpressionNode ass) { + IdentifierNode varNameNode = ass.getVarNameNode(); + // Note: 对于数组元素的赋值,不做处理 + if (varNameNode != null) { + String varName = varNameNode.getText(); + PsiClass varType = ass.getVarType(); + + XLangVarDecl varDecl = new XLangVarDecl(varType, varNameNode); + + return Map.of(varName, varDecl); } } + return Map.of(); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java index 5f750aaba..da84e8624 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java @@ -3,13 +3,40 @@ package io.nop.idea.plugin.lang.script.psi; import java.util.Map; import com.intellij.lang.ASTNode; -import com.intellij.psi.PsiElement; +import io.nop.idea.plugin.lang.XLangVarDecl; import org.jetbrains.annotations.NotNull; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_moduleDeclaration_import; + /** * 各种语句的根节点 *

- * 赋值、函数、导入、try 等语句,各自均有唯一的根节点 + * 赋值、函数、导入、try 等语句,各自均有唯一的根节点: + *

+ * TopLevelStatementNode(ast_topLevelStatement)
+ *   RuleSpecNode(moduleDeclaration_import)
+ *     ImportDeclarationNode(importAsDeclaration)
+ * 
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ *   StatementNode(statement)
+ *     VariableDeclarationNode(variableDeclaration)
+ * 
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ *   StatementNode(statement)
+ *     FunctionDeclarationNode(functionDeclaration)
+ * 
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ *   StatementNode(statement)
+ *     RuleSpecNode(ifStatement)
+ * 
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ *   StatementNode(statement)
+ *     RuleSpecNode(expressionStatement)
+ * 
* * @author flytreeleft * @date 2025-06-30 @@ -21,16 +48,15 @@ public class TopLevelStatementNode extends RuleSpecNode { } @Override - public @NotNull Map getVarTypes() { - PsiElement firstChild = getFirstChild(); + public @NotNull Map getVars() { + RuleSpecNode firstChild = (RuleSpecNode) getFirstChild(); if (firstChild instanceof StatementNode s) { - return s.getVarTypes(); + return s.getVars(); } // - else if (firstChild.getFirstChild() instanceof ImportDeclarationNode i) { - return i.getVarTypes(); + else if (firstChild.isRuleType(RULE_moduleDeclaration_import)) { + return ((ImportDeclarationNode) firstChild.getFirstChild()).getVars(); } - return Map.of(); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java index 9187bcbde..983956658 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java @@ -3,42 +3,59 @@ package io.nop.idea.plugin.lang.script.psi; import java.util.Map; import com.intellij.lang.ASTNode; +import io.nop.idea.plugin.lang.XLangVarDecl; import org.jetbrains.annotations.NotNull; /** * 含 constlet 语句 + *

+ * 多变量声明 let abc = 123, def = 456;: + *

+ * VariableDeclarationNode(variableDeclaration)
+ *   RuleSpecNode(varModifier_)
+ *     PsiElement('let')('let')
+ *   VariableDeclaratorsNode(variableDeclarators_)
+ *     VariableDeclaratorNode(variableDeclarator)
+ *       RuleSpecNode(ast_identifierOrPattern)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('abc')
+ *       RuleSpecNode(expression_initializer)
+ *         PsiElement('=')('=')
+ *         ExpressionNode(expression_single)
+ *           LiteralNode(literal)
+ *             RuleSpecNode(literal_numeric)
+ *               PsiElement(DecimalIntegerLiteral)('123')
+ *     PsiElement(',')(',')
+ *     VariableDeclaratorNode(variableDeclarator)
+ *       RuleSpecNode(ast_identifierOrPattern)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('def')
+ *       RuleSpecNode(expression_initializer)
+ *         PsiElement('=')('=')
+ *         ExpressionNode(expression_single)
+ *           ExpressionNode(expression_single)
+ *             LiteralNode(literal)
+ *               RuleSpecNode(literal_numeric)
+ *                 PsiElement(DecimalIntegerLiteral)('456')
+ *   RuleSpecNode(eos__)
+ *     PsiElement(';')(';')
+ * 
* * @author flytreeleft * @date 2025-06-30 */ public class VariableDeclarationNode extends RuleSpecNode { + private VariableDeclaratorsNode declarators; public VariableDeclarationNode(@NotNull ASTNode node) { super(node); } @Override - public @NotNull Map getVarTypes() { - /* 变量声明:let def = 123; - VariableDeclarationNode(variableDeclaration) - RuleSpecNode(varModifier_) - PsiElement('let')('let') - VariableDeclaratorsNode(variableDeclarators_) - VariableDeclaratorNode(variableDeclarator) - RuleSpecNode(ast_identifierOrPattern) - IdentifierNode(identifier) - PsiElement(Identifier)('def') - RuleSpecNode(expression_initializer) - PsiElement('=')('=') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('123') - RuleSpecNode(eos__) - PsiElement(';')(';') - */ - VariableDeclaratorsNode declarators = findChildByClass(VariableDeclaratorsNode.class); - - return declarators.getVarTypes(); + public @NotNull Map getVars() { + if (declarators == null) { + declarators = findChildByClass(VariableDeclaratorsNode.class); + } + return declarators == null ? Map.of() : declarators.getVars(); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java index 3f7d76541..bd466c343 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java @@ -4,38 +4,68 @@ import java.util.Map; import com.intellij.lang.ASTNode; import com.intellij.psi.PsiClass; +import io.nop.idea.plugin.lang.XLangVarDecl; import org.jetbrains.annotations.NotNull; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_ast_identifierOrPattern; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_expression_initializer; + /** + * abc = 123: + *
+ * VariableDeclaratorNode(variableDeclarator)
+ *   RuleSpecNode(ast_identifierOrPattern)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('abc')
+ *   RuleSpecNode(expression_initializer)
+ *     PsiElement('=')('=')
+ *     ExpressionNode(expression_single)
+ *       LiteralNode(literal)
+ *         RuleSpecNode(literal_numeric)
+ *           PsiElement(DecimalIntegerLiteral)('123')
+ * 
+ * * @author flytreeleft * @date 2025-07-03 */ public class VariableDeclaratorNode extends RuleSpecNode { + private IdentifierNode identifier; + private ExpressionNode expression; public VariableDeclaratorNode(@NotNull ASTNode node) { super(node); } + protected IdentifierNode getIdentifier() { + if (identifier == null || !identifier.isValid()) { + RuleSpecNode node = findChildByType(RULE_ast_identifierOrPattern); + + identifier = node != null ? (IdentifierNode) node.getFirstChild() : null; + } + return identifier; + } + + protected ExpressionNode getExpression() { + if (expression == null || !expression.isValid()) { + RuleSpecNode node = findChildByType(RULE_expression_initializer); + + expression = node != null ? (ExpressionNode) node.getLastChild() : null; + } + return expression; + } + @Override - public @NotNull Map getVarTypes() { - /* 变量声明:def = 123; - VariableDeclaratorNode(variableDeclarator) - RuleSpecNode(ast_identifierOrPattern) - IdentifierNode(identifier) - PsiElement(Identifier)('def') - RuleSpecNode(expression_initializer) - PsiElement('=')('=') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('123') - */ - IdentifierNode identifier = ((RuleSpecNode) getFirstChild()).getIdentifier(); - ExpressionNode expression = (ExpressionNode) getLastChild().getLastChild(); - - String varName = identifier.getText(); - PsiClass varType = expression.getResultType(); - VarDecl varDecl = new VarDecl(identifier, varType); + public @NotNull Map getVars() { + IdentifierNode identifier = getIdentifier(); + ExpressionNode expression = getExpression(); + + String varName = identifier != null ? identifier.getText() : null; + PsiClass varType = expression != null ? expression.getResultType() : null; + if (varName == null || varType == null) { + return Map.of(); + } + + XLangVarDecl varDecl = new XLangVarDecl(varType, identifier); return Map.of(varName, varDecl); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java index 96da9d311..87e1c1ded 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java @@ -5,10 +5,38 @@ import java.util.Map; import com.intellij.lang.ASTNode; import com.intellij.psi.PsiElement; +import io.nop.idea.plugin.lang.XLangVarDecl; import org.jetbrains.annotations.NotNull; /** * 由逗号分隔的多个变量声明的节点 + *

+ * 如 abc = 123, def = 456: + *

+ * VariableDeclaratorsNode(variableDeclarators_)
+ *   VariableDeclaratorNode(variableDeclarator)
+ *     RuleSpecNode(ast_identifierOrPattern)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('abc')
+ *     RuleSpecNode(expression_initializer)
+ *       PsiElement('=')('=')
+ *       ExpressionNode(expression_single)
+ *         LiteralNode(literal)
+ *           RuleSpecNode(literal_numeric)
+ *             PsiElement(DecimalIntegerLiteral)('123')
+ *   PsiElement(',')(',')
+ *   VariableDeclaratorNode(variableDeclarator)
+ *     RuleSpecNode(ast_identifierOrPattern)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('def')
+ *     RuleSpecNode(expression_initializer)
+ *       PsiElement('=')('=')
+ *       ExpressionNode(expression_single)
+ *         ExpressionNode(expression_single)
+ *           LiteralNode(literal)
+ *             RuleSpecNode(literal_numeric)
+ *               PsiElement(DecimalIntegerLiteral)('456')
+ * 
* * @author flytreeleft * @date 2025-07-03 @@ -20,25 +48,12 @@ public class VariableDeclaratorsNode extends RuleSpecNode { } @Override - public @NotNull Map getVarTypes() { - /* 变量声明:abc = 123, def = 456; - VariableDeclaratorsNode(variableDeclarators_) - VariableDeclaratorNode(variableDeclarator) - RuleSpecNode(ast_identifierOrPattern) - IdentifierNode(identifier) - PsiElement(Identifier)('abc') - RuleSpecNode(expression_initializer) - PsiElement('=')('=') - ExpressionNode(expression_single) - LiteralNode(literal) - RuleSpecNode(literal_numeric) - PsiElement(DecimalIntegerLiteral)('123') - */ - Map vars = new HashMap<>(); + public @NotNull Map getVars() { + Map vars = new HashMap<>(); for (PsiElement child : getChildren()) { if (child instanceof VariableDeclaratorNode declarator) { - vars.putAll(declarator.getVarTypes()); + vars.putAll(declarator.getVars()); } } return vars; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassReference.java new file mode 100644 index 000000000..726f7a439 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassReference.java @@ -0,0 +1,33 @@ +package io.nop.idea.plugin.lang.script.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReferenceBase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author flytreeleft + * @date 2025-07-04 + */ +public class ClassReference extends PsiReferenceBase { + private final PsiClass clazz; + + public ClassReference( + @NotNull PsiElement element, PsiClass clazz, TextRange rangeInElement + ) { + super(element, rangeInElement); + this.clazz = clazz; + } + + @Override + public @Nullable PsiElement resolve() { + return clazz; + } + + @Override + public Object @NotNull [] getVariants() { + return super.getVariants(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/VariableDeclarationReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/VariableDeclarationReference.java new file mode 100644 index 000000000..10d0606a2 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/VariableDeclarationReference.java @@ -0,0 +1,33 @@ +package io.nop.idea.plugin.lang.script.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReferenceBase; +import io.nop.idea.plugin.lang.script.psi.IdentifierNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author flytreeleft + * @date 2025-07-04 + */ +public class VariableDeclarationReference extends PsiReferenceBase { + private final IdentifierNode identifier; + + public VariableDeclarationReference( + @NotNull PsiElement element, IdentifierNode identifier, TextRange rangeInElement + ) { + super(element, rangeInElement); + this.identifier = identifier; + } + + @Override + public @Nullable PsiElement resolve() { + return identifier; + } + + @Override + public Object @NotNull [] getVariants() { + return super.getVariants(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java index e19138521..2e031fa9c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -69,9 +69,6 @@ public class PsiClassHelper { return null; } -// String typeName = type.getCanonicalText(false); -// return PsiClassHelper.findClass(project, typeName); - // 处理通配符泛型 if (type instanceof PsiWildcardType t) { PsiType bound = t.getBound(); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index a64f4fbcd..e9697c3ac 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -24,15 +24,20 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { import java.lang.Number; // const abc = ormTemplate.findListByQuery(query, mapper); - query.addFilter(filter(query, svcCtx)); // a(1, 2); a.b.c(1, 2); // let abc = new String("abc"); - let def = 123; + let def = new String("def").trim(); + let def = 123, lmn = 456 + abc; const c = a.b.c; const def = {a, b: 1}; + const arr = [a, b, c]; + arr[0] = 'a'; + // + let xyz; + xyz = "234"; // const fn1 = (a, b) => a + b; function fn2(a, b) { @@ -81,6 +86,6 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { } protected String toParseTreeText(@NotNull PsiElement tree) { - return DebugUtil.psiToString(tree, false, false); + return DebugUtil.psiToString(tree, true, false); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast index b15d94f0a..563b739f4 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast @@ -1,10 +1,11 @@ FILE ProgramNode(program) RuleSpecNode(topLevelStatements_) - StatementRootNode(ast_topLevelStatement) + TopLevelStatementNode(ast_topLevelStatement) RuleSpecNode(moduleDeclaration_import) ImportDeclarationNode(importAsDeclaration) PsiElement('import')('import') + PsiWhiteSpace(' ') ImportSourceNode(ast_importSource) RuleSpecNode(qualifiedName) RuleSpecNode(qualifiedName_name_) @@ -22,10 +23,12 @@ FILE PsiElement(Identifier)('String') RuleSpecNode(eos__) PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) RuleSpecNode(moduleDeclaration_import) ImportDeclarationNode(importAsDeclaration) PsiElement('import')('import') + PsiWhiteSpace(' ') ImportSourceNode(ast_importSource) RuleSpecNode(qualifiedName) RuleSpecNode(qualifiedName_name_) @@ -43,19 +46,24 @@ FILE PsiElement(Identifier)('Number') RuleSpecNode(eos__) PsiElement(';')(';') + PsiWhiteSpace('\n') PsiElement(SingleLineComment)('//') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) VariableDeclarationNode(variableDeclaration) RuleSpecNode(varModifier_) PsiElement('const')('const') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('abc') + PsiWhiteSpace(' ') RuleSpecNode(expression_initializer) PsiElement('=')('=') + PsiWhiteSpace(' ') ExpressionNode(expression_single) ExpressionNode(expression_single) ExpressionNode(expression_single) @@ -72,47 +80,18 @@ FILE IdentifierNode(identifier) PsiElement(Identifier)('query') PsiElement(',')(',') + PsiWhiteSpace(' ') ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('mapper') PsiElement(')')(')') RuleSpecNode(eos__) PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) - RuleSpecNode(expressionStatement) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('query') - PsiElement('.')('.') - ObjectMemberNode(identifier_ex) - RuleSpecNode(identifierOrKeyword_) - IdentifierNode(identifier) - PsiElement(Identifier)('addFilter') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('filter') - CalleeArgumentsNode(arguments_) - PsiElement('(')('(') - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('query') - PsiElement(',')(',') - ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('svcCtx') - PsiElement(')')(')') - PsiElement(')')(')') - RuleSpecNode(eos__) - PsiElement(';')(';') + PsiWhiteSpace('\n') PsiElement(SingleLineComment)('//') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) RuleSpecNode(expressionStatement) ExpressionNode(expression_single) ExpressionNode(expression_single) @@ -125,6 +104,7 @@ FILE RuleSpecNode(literal_numeric) PsiElement(DecimalIntegerLiteral)('1') PsiElement(',')(',') + PsiWhiteSpace(' ') ExpressionNode(expression_single) LiteralNode(literal) RuleSpecNode(literal_numeric) @@ -132,8 +112,9 @@ FILE PsiElement(')')(')') RuleSpecNode(eos__) PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) RuleSpecNode(expressionStatement) ExpressionNode(expression_single) ExpressionNode(expression_single) @@ -158,6 +139,7 @@ FILE RuleSpecNode(literal_numeric) PsiElement(DecimalIntegerLiteral)('1') PsiElement(',')(',') + PsiWhiteSpace(' ') ExpressionNode(expression_single) LiteralNode(literal) RuleSpecNode(literal_numeric) @@ -165,21 +147,27 @@ FILE PsiElement(')')(')') RuleSpecNode(eos__) PsiElement(';')(';') + PsiWhiteSpace('\n') PsiElement(SingleLineComment)('//') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) VariableDeclarationNode(variableDeclaration) RuleSpecNode(varModifier_) PsiElement('let')('let') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('abc') + PsiWhiteSpace(' ') RuleSpecNode(expression_initializer) PsiElement('=')('=') + PsiWhiteSpace(' ') ExpressionNode(expression_single) PsiElement('new')('new') + PsiWhiteSpace(' ') ParameterizedTypeNode(parameterizedTypeNode) RuleSpecNode(qualifiedName_) RuleSpecNode(qualifiedName) @@ -195,36 +183,109 @@ FILE PsiElement(')')(')') RuleSpecNode(eos__) PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('def') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + PsiElement('new')('new') + PsiWhiteSpace(' ') + ParameterizedTypeNode(parameterizedTypeNode) + RuleSpecNode(qualifiedName_) + RuleSpecNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('String') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_string) + PsiElement(StringLiteral)('"def"') + PsiElement(')')(')') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('trim') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) VariableDeclarationNode(variableDeclaration) RuleSpecNode(varModifier_) PsiElement('let')('let') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('def') + PsiWhiteSpace(' ') RuleSpecNode(expression_initializer) PsiElement('=')('=') + PsiWhiteSpace(' ') ExpressionNode(expression_single) LiteralNode(literal) RuleSpecNode(literal_numeric) PsiElement(DecimalIntegerLiteral)('123') + PsiElement(',')(',') + PsiWhiteSpace(' ') + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('lmn') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('456') + PsiWhiteSpace(' ') + PsiElement('+')('+') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') RuleSpecNode(eos__) PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) VariableDeclarationNode(variableDeclaration) RuleSpecNode(varModifier_) PsiElement('const')('const') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('c') + PsiWhiteSpace(' ') RuleSpecNode(expression_initializer) PsiElement('=')('=') + PsiWhiteSpace(' ') ExpressionNode(expression_single) ExpressionNode(expression_single) ExpressionNode(expression_single) @@ -242,18 +303,22 @@ FILE PsiElement(Identifier)('c') RuleSpecNode(eos__) PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) VariableDeclarationNode(variableDeclaration) RuleSpecNode(varModifier_) PsiElement('const')('const') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('def') + PsiWhiteSpace(' ') RuleSpecNode(expression_initializer) PsiElement('=')('=') + PsiWhiteSpace(' ') ExpressionNode(expression_single) ObjectDeclarationNode(objectExpression) PsiElement('{')('{') @@ -265,6 +330,7 @@ FILE IdentifierNode(identifier) PsiElement(Identifier)('a') PsiElement(',')(',') + PsiWhiteSpace(' ') ObjectPropertyDeclarationNode(ast_objectProperty) ObjectPropertyAssignmentNode(propertyAssignment) RuleSpecNode(expression_propName) @@ -273,6 +339,7 @@ FILE IdentifierNode(identifier) PsiElement(Identifier)('b') PsiElement(':')(':') + PsiWhiteSpace(' ') ExpressionNode(expression_single) LiteralNode(literal) RuleSpecNode(literal_numeric) @@ -280,19 +347,121 @@ FILE PsiElement('}')('}') RuleSpecNode(eos__) PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('arr') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + ArrayExpressionNode(arrayExpression) + PsiElement('[')('[') + RuleSpecNode(elementList_) + RuleSpecNode(ast_arrayElement) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiElement(',')(',') + PsiWhiteSpace(' ') + RuleSpecNode(ast_arrayElement) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiElement(',')(',') + PsiWhiteSpace(' ') + RuleSpecNode(ast_arrayElement) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + PsiElement(']')(']') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + AssignmentExpressionNode(assignmentExpression) + RuleSpecNode(expression_leftHandSide) + RuleSpecNode(memberExpression) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('arr') + PsiElement('[')('[') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('0') + PsiElement(']')(']') + PsiWhiteSpace(' ') + RuleSpecNode(assignmentOperator_) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_string) + PsiElement(StringLiteral)(''a'') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + PsiElement(SingleLineComment)('//') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('xyz') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + AssignmentExpressionNode(assignmentExpression) + RuleSpecNode(expression_leftHandSide) + IdentifierNode(identifier) + PsiElement(Identifier)('xyz') + PsiWhiteSpace(' ') + RuleSpecNode(assignmentOperator_) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_string) + PsiElement(StringLiteral)('"234"') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') PsiElement(SingleLineComment)('//') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) VariableDeclarationNode(variableDeclaration) RuleSpecNode(varModifier_) PsiElement('const')('const') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('fn1') + PsiWhiteSpace(' ') RuleSpecNode(expression_initializer) PsiElement('=')('=') + PsiWhiteSpace(' ') ExpressionNode(expression_single) ArrowFunctionNode(arrowFunctionExpression) PsiElement('(')('(') @@ -302,27 +471,34 @@ FILE IdentifierNode(identifier) PsiElement(Identifier)('a') PsiElement(',')(',') + PsiWhiteSpace(' ') FunctionParameterDeclarationNode(parameterDeclaration) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('b') PsiElement(')')(')') + PsiWhiteSpace(' ') PsiElement('=>')('=>') + PsiWhiteSpace(' ') ArrowFunctionBodyNode(expression_functionBody) ExpressionNode(expression_single) ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('a') + PsiWhiteSpace(' ') PsiElement('+')('+') + PsiWhiteSpace(' ') ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('b') RuleSpecNode(eos__) PsiElement(';')(';') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) FunctionDeclarationNode(functionDeclaration) PsiElement('function')('function') + PsiWhiteSpace(' ') IdentifierNode(identifier) PsiElement(Identifier)('fn2') PsiElement('(')('(') @@ -332,54 +508,71 @@ FILE IdentifierNode(identifier) PsiElement(Identifier)('a') PsiElement(',')(',') + PsiWhiteSpace(' ') FunctionParameterDeclarationNode(parameterDeclaration) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('b') PsiElement(')')(')') + PsiWhiteSpace(' ') BlockStatementNode(blockStatement) PsiElement('{')('{') + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') RuleSpecNode(statements_) - RuleSpecNode(statement) + StatementNode(statement) VariableDeclarationNode(variableDeclaration) RuleSpecNode(varModifier_) PsiElement('const')('const') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('c') + PsiWhiteSpace(' ') RuleSpecNode(expression_initializer) PsiElement('=')('=') + PsiWhiteSpace(' ') ExpressionNode(expression_single) LiteralNode(literal) RuleSpecNode(literal_numeric) PsiElement(DecimalIntegerLiteral)('5') RuleSpecNode(eos__) PsiElement(';')(';') - RuleSpecNode(statement) - RuleSpecNode(returnStatement) + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') + StatementNode(statement) + ReturnStatementNode(returnStatement) PsiElement('return')('return') + PsiWhiteSpace(' ') ExpressionNode(expression_single) ExpressionNode(expression_single) ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('a') + PsiWhiteSpace(' ') PsiElement('+')('+') + PsiWhiteSpace(' ') ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('b') + PsiWhiteSpace(' ') PsiElement('+')('+') + PsiWhiteSpace(' ') ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('c') RuleSpecNode(eos__) PsiElement(';')(';') + PsiWhiteSpace('\n') PsiElement('}')('}') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) FunctionDeclarationNode(functionDeclaration) PsiElement('function')('function') + PsiWhiteSpace(' ') IdentifierNode(identifier) PsiElement(Identifier)('fn3') PsiElement('(')('(') @@ -390,75 +583,98 @@ FILE PsiElement(Identifier)('a') RuleSpecNode(namedTypeNode_annotation) PsiElement(':')(':') + PsiWhiteSpace(' ') RuleSpecNode(namedTypeNode) RuleSpecNode(typeNameNode_predefined) PsiElement('string')('string') PsiElement(',')(',') + PsiWhiteSpace(' ') FunctionParameterDeclarationNode(parameterDeclaration) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('b') RuleSpecNode(namedTypeNode_annotation) PsiElement(':')(':') + PsiWhiteSpace(' ') RuleSpecNode(namedTypeNode) RuleSpecNode(typeNameNode_predefined) PsiElement('number')('number') PsiElement(')')(')') + PsiWhiteSpace(' ') BlockStatementNode(blockStatement) PsiElement('{')('{') + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') RuleSpecNode(statements_) - RuleSpecNode(statement) - RuleSpecNode(returnStatement) + StatementNode(statement) + ReturnStatementNode(returnStatement) PsiElement('return')('return') + PsiWhiteSpace(' ') ExpressionNode(expression_single) ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('a') + PsiWhiteSpace(' ') PsiElement('+')('+') + PsiWhiteSpace(' ') ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('b') RuleSpecNode(eos__) PsiElement(';')(';') + PsiWhiteSpace('\n') PsiElement('}')('}') + PsiWhiteSpace('\n') PsiElement(SingleLineComment)('//') - StatementRootNode(ast_topLevelStatement) - RuleSpecNode(statement) + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) RuleSpecNode(ifStatement) PsiElement('if')('if') + PsiWhiteSpace(' ') PsiElement('(')('(') ExpressionNode(expression_single) ExpressionNode(expression_single) IdentifierNode(identifier) PsiElement(Identifier)('a') + PsiWhiteSpace(' ') PsiElement('>')('>') + PsiWhiteSpace(' ') ExpressionNode(expression_single) LiteralNode(literal) RuleSpecNode(literal_numeric) PsiElement(DecimalIntegerLiteral)('2') PsiElement(')')(')') - RuleSpecNode(statement) + PsiWhiteSpace(' ') + StatementNode(statement) BlockStatementNode(blockStatement) PsiElement('{')('{') + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') RuleSpecNode(statements_) - RuleSpecNode(statement) + StatementNode(statement) VariableDeclarationNode(variableDeclaration) RuleSpecNode(varModifier_) PsiElement('let')('let') - RuleSpecNode(variableDeclarators_) - RuleSpecNode(variableDeclarator) + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) PsiElement(Identifier)('b') + PsiWhiteSpace(' ') RuleSpecNode(expression_initializer) PsiElement('=')('=') + PsiWhiteSpace(' ') ExpressionNode(expression_single) LiteralNode(literal) RuleSpecNode(literal_numeric) PsiElement(DecimalIntegerLiteral)('3') RuleSpecNode(eos__) PsiElement(';')(';') - RuleSpecNode(statement) + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') + StatementNode(statement) RuleSpecNode(expressionStatement) ExpressionNode(expression_single) ExpressionNode(expression_single) @@ -476,6 +692,7 @@ FILE IdentifierNode(identifier) PsiElement(Identifier)('b') PsiElement(',')(',') + PsiWhiteSpace(' ') ExpressionNode(expression_single) LiteralNode(literal) RuleSpecNode(literal_numeric) @@ -483,4 +700,6 @@ FILE PsiElement(')')(')') RuleSpecNode(eos__) PsiElement(';')(';') + PsiWhiteSpace('\n') PsiElement('}')('}') + PsiWhiteSpace('\n') -- Gitee From 23ba716e915a76132497f1f62cc157b14ead5618 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sat, 5 Jul 2025 13:49:18 +0800 Subject: [PATCH 34/82] =?UTF-8?q?nop-idea-pugin:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=8E=20XLang=20Script=20=E4=B8=AD=E7=9A=84=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E8=AF=86=E5=88=AB=E7=9B=B8=E5=85=B3=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../script/XLangScriptParserDefinition.java | 3 + .../lang/script/XLangScriptTokenTypes.java | 1 + .../lang/script/psi/ExpressionNode.java | 74 ++++-------- .../script/psi/FunctionDeclarationNode.java | 6 +- .../lang/script/psi/IdentifierNode.java | 27 +++-- .../script/psi/ImportDeclarationNode.java | 18 ++- .../lang/script/psi/ImportSourceNode.java | 51 ++++---- .../plugin/lang/script/psi/LiteralNode.java | 10 +- .../script/psi/ObjectDeclarationNode.java | 67 +++++++++- .../psi/ObjectPropertyAssignmentNode.java | 43 +++++++ .../script/psi/ParameterizedTypeNode.java | 110 +++++++++++++++++ .../lang/script/psi/QualifiedNameNode.java | 53 ++++++++ .../plugin/lang/script/psi/RuleSpecNode.java | 21 ++-- .../plugin/lang/script/psi/StatementNode.java | 1 - .../script/psi/VariableDeclaratorNode.java | 3 +- ...sReference.java => PsiClassReference.java} | 4 +- ...yReference.java => PsiFieldReference.java} | 4 +- ...Reference.java => PsiMethodReference.java} | 4 +- .../nop/idea/plugin/utils/PsiClassHelper.java | 67 +++++++--- .../plugin/lang/TestXLangScriptParser.java | 2 + .../lang/TestXLangScriptReferences.java | 114 ++++++++++++++---- .../resources/_vfs/test/ast/statement-1.ast | 99 +++++++++++++-- .../_vfs/test/java/XJsonDomainHandler.java | 15 +++ 23 files changed, 618 insertions(+), 179 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/{ClassReference.java => PsiClassReference.java} (88%) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/{ClassPropertyReference.java => PsiFieldReference.java} (79%) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/{ClassMethodReference.java => PsiMethodReference.java} (80%) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index 3dbac8b39..1533ca1d8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -29,6 +29,7 @@ import io.nop.idea.plugin.lang.script.psi.ObjectPropertyAssignmentNode; import io.nop.idea.plugin.lang.script.psi.ObjectPropertyDeclarationNode; import io.nop.idea.plugin.lang.script.psi.ParameterizedTypeNode; import io.nop.idea.plugin.lang.script.psi.ProgramNode; +import io.nop.idea.plugin.lang.script.psi.QualifiedNameNode; import io.nop.idea.plugin.lang.script.psi.ReturnStatementNode; import io.nop.idea.plugin.lang.script.psi.RuleSpecNode; import io.nop.idea.plugin.lang.script.psi.StatementNode; @@ -89,6 +90,8 @@ public class XLangScriptParserDefinition implements ParserDefinition { new ImportDeclarationNode(node); case XLangParser.RULE_ast_importSource -> // new ImportSourceNode(node); + case XLangParser.RULE_qualifiedName -> // + new QualifiedNameNode(node); // case XLangParser.RULE_variableDeclaration -> // new VariableDeclarationNode(node); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java index d717c2af2..4d9da1ac2 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java @@ -44,6 +44,7 @@ public class XLangScriptTokenTypes { public static final RuleIElementType RULE_expression_initializer = rule(XLangParser.RULE_expression_initializer); public static final RuleIElementType RULE_moduleDeclaration_import = rule(XLangParser.RULE_moduleDeclaration_import); + public static final RuleIElementType RULE_objectProperties = rule(XLangParser.RULE_objectProperties_); public static TokenSet tokenSet(int... types) { return PSIElementTypeFactory.createTokenSet(XLangScriptLanguage.INSTANCE, types); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index 59aeb4b2d..add9dff5d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -12,9 +12,8 @@ import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiType; import com.intellij.psi.impl.PsiClassImplUtil; -import com.intellij.psi.util.PsiTreeUtil; -import io.nop.idea.plugin.lang.script.reference.ClassMethodReference; -import io.nop.idea.plugin.lang.script.reference.ClassPropertyReference; +import io.nop.idea.plugin.lang.script.reference.PsiFieldReference; +import io.nop.idea.plugin.lang.script.reference.PsiMethodReference; import org.jetbrains.annotations.NotNull; /** @@ -272,27 +271,17 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-30 */ public class ExpressionNode extends RuleSpecNode { - private PsiClass[] resultType; public ExpressionNode(@NotNull ASTNode node) { super(node); } - /** - * 获取表达式结果的类型 - * - * @return null 为有效值 - */ - public PsiClass getResultType() { - if (resultType == null) { - resultType = new PsiClass[] { doGetResultType() }; - } - return resultType[0]; - } - @Override public PsiReference @NotNull [] doGetReferences() { - // Note: 仅识别当前表达式的最后一个有效元素的引用,其余部分,由其子表达式做识别处理 + // Note: + // - 仅识别当前表达式的最后一个有效元素的引用,其余部分,由其子表达式做识别处理 + // - 对象声明节点 ObjectDeclarationNode 的相关引用,由其自身负责构造 + // - 对构造函数 ParameterizedTypeNode 的相关引用,由其自身负责构造 PsiElement firstChild = getFirstChild(); // 变量引用:abc @@ -301,28 +290,15 @@ public class ExpressionNode extends RuleSpecNode { return i.createReferences(this, textRange); } - // 构造函数:new String("abc") - else if (isObjectConstructorCall()) { - ParameterizedTypeNode cons = PsiTreeUtil.findChildOfType(this, ParameterizedTypeNode.class); - TextRange textRange = cons.getTextRangeInParent(); - - IdentifierNode proto = (IdentifierNode) PsiTreeUtil.getDeepestLast(cons).getParent(); - - return proto.createReferences(this, textRange); - } - // 对象声明:{a, b: 1} - else if (isObjectDeclaration()) { - return PsiReference.EMPTY_ARRAY; - } // 对象方法调用:a.b.c(1, 2) else if (isObjectMethodCall()) { - ExpressionNode obj = (ExpressionNode) getFirstChild(); + ExpressionNode obj = (ExpressionNode) firstChild; PsiMethod method = getObjectMethod(); if (method != null) { // Note: 需加上相对于当前表达式的对象偏移量 TextRange methodTextRange = obj.getObjectMemberTextRange().shiftLeft(obj.getStartOffsetInParent()); - ClassMethodReference ref = new ClassMethodReference(this, method, methodTextRange); + PsiMethodReference ref = new PsiMethodReference(this, method, methodTextRange); return new PsiReference[] { ref }; } @@ -333,14 +309,14 @@ public class ExpressionNode extends RuleSpecNode { if (prop != null) { TextRange propTextRange = getObjectMemberTextRange(); - ClassPropertyReference ref = new ClassPropertyReference(this, prop, propTextRange); + PsiFieldReference ref = new PsiFieldReference(this, prop, propTextRange); return new PsiReference[] { ref }; } } // 函数调用:fn1(1, 2, 3) else if (isFunctionCall()) { - ExpressionNode callee = (ExpressionNode) getFirstChild(); + ExpressionNode callee = (ExpressionNode) firstChild; TextRange textRange = callee.getTextRangeInParent(); IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); @@ -351,7 +327,12 @@ public class ExpressionNode extends RuleSpecNode { return PsiReference.EMPTY_ARRAY; } - protected PsiClass doGetResultType() { + /** + * 获取表达式结果的类型 + * + * @return null 为有效值 + */ + public PsiClass getResultType() { PsiElement firstChild = getFirstChild(); if (firstChild instanceof LiteralNode l) { @@ -361,10 +342,9 @@ public class ExpressionNode extends RuleSpecNode { return i.getDataType(); } // else if (isObjectConstructorCall()) { - ParameterizedTypeNode cons = PsiTreeUtil.findChildOfType(this, ParameterizedTypeNode.class); - IdentifierNode proto = (IdentifierNode) PsiTreeUtil.getDeepestLast(cons).getParent(); + ParameterizedTypeNode cons = findChildByClass(ParameterizedTypeNode.class); - return proto.getDataType(); + return cons != null ? cons.getParameterizedType() : null; } // else if (isObjectMethodCall()) { PsiMethod method = getObjectMethod(); @@ -380,19 +360,19 @@ public class ExpressionNode extends RuleSpecNode { return getPsiClassByPsiType(propType); } // else if (isFunctionCall()) { - ExpressionNode callee = (ExpressionNode) getFirstChild(); + ExpressionNode callee = (ExpressionNode) firstChild; IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); // Note: 对应的是函数的返回值类型 return fn.getDataType(); } // else if (isArrowFunction()) { - ArrowFunctionNode fn = (ArrowFunctionNode) getFirstChild(); + ArrowFunctionNode fn = (ArrowFunctionNode) firstChild; return fn.getReturnType(); } // else if (isArrayInit()) { - ArrayExpressionNode array = (ArrayExpressionNode) getFirstChild(); + ArrayExpressionNode array = (ArrayExpressionNode) firstChild; return array.getElementType(); } @@ -410,18 +390,6 @@ public class ExpressionNode extends RuleSpecNode { return false; } - /** - * 当前表达式是否为对象声明 - *

- * {@link ObjectDeclarationNode} 的直接父节点为 {@link ExpressionNode}, - * 因此,在构造 {@link ObjectMemberNode} 的成员引用时, - * 一般不为对象声明中的属性构造引用 - */ - public boolean isObjectDeclaration() { - // {a, b: 1} - return getFirstChild() instanceof ObjectDeclarationNode; - } - /** 当前表达式是否为对象方法调用 */ public boolean isObjectMethodCall() { // a.b.c() diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java index 0856c4852..7330bcc43 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java @@ -60,7 +60,6 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-30 */ public class FunctionDeclarationNode extends RuleSpecNode { - private PsiClass[] returnType; public FunctionDeclarationNode(@NotNull ASTNode node) { super(node); @@ -73,10 +72,7 @@ public class FunctionDeclarationNode extends RuleSpecNode { /** 获取函数的返回值类型 */ public PsiClass getReturnType() { // TODO 分析函数的 return 表达式,得到返回类型 - if (returnType == null) { - returnType = new PsiClass[] {}; - } - return returnType[0]; + return null; } /** 参数列表为函数内可访问的变量 */ diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java index bb34714ab..9aed09485 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java @@ -5,9 +5,11 @@ import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; +import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.lang.XLangVarDecl; -import io.nop.idea.plugin.lang.script.reference.ClassReference; +import io.nop.idea.plugin.lang.script.reference.PsiClassReference; import io.nop.idea.plugin.lang.script.reference.VariableDeclarationReference; +import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; /** @@ -23,7 +25,6 @@ import org.jetbrains.annotations.NotNull; * @date 2025-07-02 */ public class IdentifierNode extends RuleSpecNode { - private XLangVarDecl[] varDecl; public IdentifierNode(@NotNull ASTNode node) { super(node); @@ -38,7 +39,7 @@ public class IdentifierNode extends RuleSpecNode { return new PsiReference[] { ref }; } else if (element instanceof PsiClass clazz) { - ClassReference ref = new ClassReference(source, clazz, textRange); + PsiClassReference ref = new PsiClassReference(source, clazz, textRange); return new PsiReference[] { ref }; } @@ -57,13 +58,21 @@ public class IdentifierNode extends RuleSpecNode { } public XLangVarDecl getVarDecl() { - if (varDecl == null) { - String varName = getText(); + String varName = getText(); + XLangVarDecl decl = findVisibleVar(varName); - varDecl = new XLangVarDecl[] { - findVisibleVar(varName) - }; + if (decl == null // + && varName.indexOf('.') < 0 // + && varName.indexOf('$') < 0 // + && StringHelper.isValidClassName(varName) // + ) { + // Note: java.lang 中的类不需要显式导入 + PsiClass clazz = PsiClassHelper.findClass(getProject(), "java.lang." + varName); + if (clazz != null) { + decl = new XLangVarDecl(clazz, clazz); + } } - return varDecl[0]; + + return decl; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java index be97bc162..3bc979a75 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java @@ -36,7 +36,6 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-29 */ public class ImportDeclarationNode extends RuleSpecNode { - private PsiClass[] clazz; public ImportDeclarationNode(@NotNull ASTNode node) { super(node); @@ -45,23 +44,20 @@ public class ImportDeclarationNode extends RuleSpecNode { @Override public @NotNull Map getVars() { ImportSourceNode imp = findChildByClass(ImportSourceNode.class); - if (imp == null) { + + QualifiedNameNode qualifiedName = imp != null ? imp.getQualifiedName() : null; + if (qualifiedName == null) { return Map.of(); } + String classFQN = qualifiedName.getFullyName(); + PsiClass clazz = PsiClassHelper.findClass(getProject(), classFQN); if (clazz == null) { - String classFQN = imp.getFullyQualifiedName(); - - clazz = new PsiClass[] { - PsiClassHelper.findClass(getProject(), classFQN) - }; - } - if (clazz[0] == null) { return Map.of(); } - String varName = imp.getLastQualifiedName(); - XLangVarDecl varDecl = new XLangVarDecl(clazz[0], clazz[0]); + String varName = qualifiedName.getLastName(); + XLangVarDecl varDecl = new XLangVarDecl(clazz, clazz); return Map.of(varName, varDecl); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java index d9c7fd0c0..b786cf271 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -1,48 +1,55 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; -import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; -import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceProvider; -import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceSet; -import com.intellij.psi.util.PsiTreeUtil; +import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; /** * import 语句中导入的类名的节点 + *

+ * java.lang.String: + *

+ * ImportSourceNode(ast_importSource)
+ *   QualifiedNameNode(qualifiedName)
+ *     RuleSpecNode(qualifiedName_name_)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('java')
+ *     PsiElement('.')('.')
+ *     QualifiedNameNode(qualifiedName)
+ *       RuleSpecNode(qualifiedName_name_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('lang')
+ *       PsiElement('.')('.')
+ *       QualifiedNameNode(qualifiedName)
+ *         RuleSpecNode(qualifiedName_name_)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('String')
+ * 
* * @author flytreeleft * @date 2025-06-29 */ public class ImportSourceNode extends RuleSpecNode { - private static final JavaClassReferenceProvider provider = new JavaClassReferenceProvider(); - - static { - // 支持解析包名:JavaClassReference#advancedResolveInner - provider.setOption(JavaClassReferenceProvider.ADVANCED_RESOLVE, true); - } + private QualifiedNameNode qualifiedName; public ImportSourceNode(@NotNull ASTNode node) { super(node); } - public String getLastQualifiedName() { - PsiElement last = PsiTreeUtil.getDeepestLast(this); - - return last.getText(); - } - - public String getFullyQualifiedName() { - return getText(); + public QualifiedNameNode getQualifiedName() { + if (qualifiedName == null || !qualifiedName.isValid()) { + qualifiedName = (QualifiedNameNode) getFirstChild(); + } + return qualifiedName; } /** 构造 Java 相关的引用对象,从而支持自动补全、引用跳转、文档显示等 */ @Override protected PsiReference @NotNull [] doGetReferences() { - String fqn = getFullyQualifiedName(); - - JavaClassReferenceSet refSet = new JavaClassReferenceSet(fqn, this, 0, false, provider); + QualifiedNameNode qnn = getQualifiedName(); + String fqn = qnn.getFullyName(); - return refSet.getReferences(); + return PsiClassHelper.createJavaClassReferences(fqn, this, 0); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java index 5c53ff46e..a1d1a1b90 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java @@ -22,7 +22,6 @@ import org.jetbrains.annotations.NotNull; */ public class LiteralNode extends RuleSpecNode { private LeafPsiElement literal; - private PsiClass[] dataType; public LiteralNode(@NotNull ASTNode node) { super(node); @@ -31,7 +30,6 @@ public class LiteralNode extends RuleSpecNode { public LeafPsiElement getLiteral() { if (literal == null || !literal.isValid()) { literal = (LeafPsiElement) PsiTreeUtil.getDeepestLast(this); - dataType = null; } return literal; } @@ -39,12 +37,8 @@ public class LiteralNode extends RuleSpecNode { /** 获取字面量的数据类型 */ public PsiClass getDataType() { LeafPsiElement target = getLiteral(); + TokenIElementType token = (TokenIElementType) target.getElementType(); - if (dataType == null) { - TokenIElementType token = (TokenIElementType) target.getElementType(); - - dataType = new PsiClass[] { getPsiClassByToken(token) }; - } - return dataType[0]; + return getPsiClassByToken(token); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java index 27f671f41..6233cf5cf 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java @@ -1,12 +1,47 @@ package io.nop.idea.plugin.lang.script.psi; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; import org.jetbrains.annotations.NotNull; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_objectProperties; + /** - * 对象声明节点,如: + * 对象声明节点 + *

+ * {a, b: 1}: *

- * {a, b: 1}
+ * ObjectDeclarationNode(objectExpression)
+ *   PsiElement('{')('{')
+ *   RuleSpecNode(objectProperties_)
+ *     ObjectPropertyDeclarationNode(ast_objectProperty)
+ *       ObjectPropertyAssignmentNode(propertyAssignment)
+ *         ObjectMemberNode(identifier_ex)
+ *           RuleSpecNode(identifierOrKeyword_)
+ *             IdentifierNode(identifier)
+ *               PsiElement(Identifier)('a')
+ *     PsiElement(',')(',')
+ *     PsiWhiteSpace(' ')
+ *     ObjectPropertyDeclarationNode(ast_objectProperty)
+ *       ObjectPropertyAssignmentNode(propertyAssignment)
+ *         RuleSpecNode(expression_propName)
+ *           ObjectMemberNode(identifier_ex)
+ *             RuleSpecNode(identifierOrKeyword_)
+ *               IdentifierNode(identifier)
+ *                 PsiElement(Identifier)('b')
+ *         PsiElement(':')(':')
+ *         PsiWhiteSpace(' ')
+ *         ExpressionNode(expression_single)
+ *           LiteralNode(literal)
+ *             RuleSpecNode(literal_numeric)
+ *               PsiElement(DecimalIntegerLiteral)('1')
+ *   PsiElement('}')('}')
  * 
* * @author flytreeleft @@ -17,4 +52,32 @@ public class ObjectDeclarationNode extends RuleSpecNode { public ObjectDeclarationNode(@NotNull ASTNode node) { super(node); } + + @Override + public PsiReference @NotNull [] doGetReferences() { + RuleSpecNode props = findChildByType(RULE_objectProperties); + if (props == null) { + return PsiReference.EMPTY_ARRAY; + } + + List result = new ArrayList<>(); + for (PsiElement child : props.getChildren()) { + if (!(child instanceof ObjectPropertyDeclarationNode propDecl)) { + continue; + } + + ObjectPropertyAssignmentNode prop = (ObjectPropertyAssignmentNode) propDecl.getFirstChild(); + if (!prop.isShorthand()) { + continue; + } + + TextRange textRange = propDecl.getTextRangeInParent(); + IdentifierNode propNameNode = prop.getPropNameNode(); + + PsiReference[] refs = propNameNode.createReferences(this, textRange); + Collections.addAll(result, refs); + } + + return result.toArray(PsiReference.EMPTY_ARRAY); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java index 800f03879..357896c64 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java @@ -1,12 +1,38 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; import org.jetbrains.annotations.NotNull; /** * 对象的属性赋值节点 *

* 如 {a, b: 1} 中,ab: 1 均为该类型节点 + *

+ * a: + *

+ * ObjectPropertyAssignmentNode(propertyAssignment)
+ *   ObjectMemberNode(identifier_ex)
+ *     RuleSpecNode(identifierOrKeyword_)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('a')
+ * 
+ * + * b: 1: + *
+ * ObjectPropertyAssignmentNode(propertyAssignment)
+ *   RuleSpecNode(expression_propName)
+ *     ObjectMemberNode(identifier_ex)
+ *       RuleSpecNode(identifierOrKeyword_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('b')
+ *   PsiElement(':')(':')
+ *   PsiWhiteSpace(' ')
+ *   ExpressionNode(expression_single)
+ *     LiteralNode(literal)
+ *       RuleSpecNode(literal_numeric)
+ *         PsiElement(DecimalIntegerLiteral)('1')
+ * 
* * @author flytreeleft * @date 2025-06-30 @@ -16,4 +42,21 @@ public class ObjectPropertyAssignmentNode extends RuleSpecNode { public ObjectPropertyAssignmentNode(@NotNull ASTNode node) { super(node); } + + public IdentifierNode getPropNameNode() { + PsiElement child = getFirstChild(); + + while (child != null) { + if (child instanceof IdentifierNode i) { + return i; + } + child = child.getFirstChild(); + } + return null; + } + + /** 是否为属性名与变量名相同时的简写形式 */ + public boolean isShorthand() { + return getFirstChild() instanceof ObjectMemberNode; + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java index 209f26d44..d44474c35 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java @@ -1,17 +1,127 @@ package io.nop.idea.plugin.lang.script.psi; +import java.util.ArrayList; +import java.util.List; + import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import io.nop.idea.plugin.lang.script.reference.PsiClassReference; +import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; /** * new 语句中的类名节点 + *

+ * String: + *

+ * ParameterizedTypeNode(parameterizedTypeNode)
+ *   RuleSpecNode(qualifiedName_)
+ *     QualifiedNameNode(qualifiedName)
+ *       RuleSpecNode(qualifiedName_name_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('String')
+ * 
+ * + * Abc.Def: + *
+ * ParameterizedTypeNode(parameterizedTypeNode)
+ *   RuleSpecNode(qualifiedName_)
+ *     QualifiedNameNode(qualifiedName)
+ *       RuleSpecNode(qualifiedName_name_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('Abc')
+ *       PsiElement('.')('.')
+ *       QualifiedNameNode(qualifiedName)
+ *         RuleSpecNode(qualifiedName_name_)
+ *           IdentifierNode(identifier)
+ *             PsiElement(Identifier)('Def')
+ * 
* * @author flytreeleft * @date 2025-06-30 */ public class ParameterizedTypeNode extends RuleSpecNode { + private QualifiedNameNode qualifiedName; public ParameterizedTypeNode(@NotNull ASTNode node) { super(node); } + + public QualifiedNameNode getQualifiedName() { + if (qualifiedName == null || !qualifiedName.isValid()) { + qualifiedName = (QualifiedNameNode) getFirstChild().getFirstChild(); + } + return qualifiedName; + } + + @Override + protected PsiReference @NotNull [] doGetReferences() { + List result = getClassAndTextRanges(); + + // 若未找到已导入的类,则尝试按包查找 + if (result.isEmpty()) { + String fqn = getText(); + + return PsiClassHelper.createJavaClassReferences(fqn, this, 0); + } + + return result.stream() // + .map(r -> new PsiClassReference(this, r.clazz, r.textRange)) // + .toArray(PsiReference[]::new); + } + + public PsiClass getParameterizedType() { + List result = getClassAndTextRanges(); + + String fqn = getText().replace(" ", ""); + if (result.isEmpty()) { + // 按全类名处理 + return PsiClassHelper.findClass(getProject(), fqn); + } + + PsiClass clazz = result.get(result.size() - 1).clazz; + String clazzName = clazz.getQualifiedName(); + + return clazzName != null // + && (fqn.equals(clazzName) // + || clazzName.endsWith('.' + fqn)) // + ? clazz : null; + } + + protected List getClassAndTextRanges() { + QualifiedNameNode qnn = getQualifiedName(); + IdentifierNode identifier = qnn.getIdentifier(); + + PsiClass clazz = identifier.getDataType(); + + List result = new ArrayList<>(); + findInnerClass(clazz, qnn, 0, result); + + return result; + } + + protected void findInnerClass( + PsiClass clazz, QualifiedNameNode qnn, int offset, List result + ) { + if (clazz == null) { + return; + } + + result.add(new PsiClassAndTextRange(clazz, qnn.getTextRangeInParent().shiftRight(offset))); + + PsiElement sub = qnn.getLastChild(); + if (!(sub instanceof QualifiedNameNode subQnn)) { + return; + } + + String subName = subQnn.getIdentifier().getText(); + PsiClass subClazz = clazz.findInnerClassByName(subName, true); + + findInnerClass(subClazz, subQnn, qnn.getStartOffsetInParent(), result); + } + + public record PsiClassAndTextRange(PsiClass clazz, TextRange textRange) {} } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java new file mode 100644 index 000000000..2994e06c5 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java @@ -0,0 +1,53 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; + +/** + * java.lang.String: + *
+ * QualifiedNameNode(qualifiedName)
+ *   RuleSpecNode(qualifiedName_name_)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('java')
+ *   PsiElement('.')('.')
+ *   QualifiedNameNode(qualifiedName)
+ *     RuleSpecNode(qualifiedName_name_)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('lang')
+ *     PsiElement('.')('.')
+ *     QualifiedNameNode(qualifiedName)
+ *       RuleSpecNode(qualifiedName_name_)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('String')
+ * 
+ * + * @author flytreeleft + * @date 2025-07-04 + */ +public class QualifiedNameNode extends RuleSpecNode { + private IdentifierNode identifier; + + public QualifiedNameNode(@NotNull ASTNode node) { + super(node); + } + + public IdentifierNode getIdentifier() { + if (identifier == null || !identifier.isValid()) { + identifier = (IdentifierNode) getFirstChild().getFirstChild(); + } + return identifier; + } + + public String getLastName() { + PsiElement last = PsiTreeUtil.getDeepestLast(this); + + return last.getText(); + } + + public String getFullyName() { + return getText(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java index fa6e92997..3ad35504c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java @@ -28,7 +28,6 @@ import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal * @date 2025-06-29 */ public class RuleSpecNode extends ASTWrapperPsiElement implements XLangVarScope { - private PsiReference[] refs; public RuleSpecNode(@NotNull ASTNode node) { super(node); @@ -42,11 +41,10 @@ public class RuleSpecNode extends ASTWrapperPsiElement implements XLangVarScope @Override public PsiReference @NotNull [] getReferences() { - if (refs == null) { - // 在没有写入动作时,才执行函数并返回结果,从而避免阻塞编辑操作 - refs = ReadAction.compute(this::doGetReferences); - } - return refs; + // Note: 不能直接缓存 PsiReference,否则,容易造成数据不一致 + + // 在没有写入动作时,才执行函数并返回结果,从而避免阻塞编辑操作 + return ReadAction.compute(this::doGetReferences); } protected PsiReference @NotNull [] doGetReferences() { @@ -66,11 +64,12 @@ public class RuleSpecNode extends ASTWrapperPsiElement implements XLangVarScope // Note: 下层的变量优先于上层的变量 if (parent instanceof RuleSpecNode) { - for (PsiElement child : parent.getChildren()) { - if (child == node) { - break; // 只取当前节点之前定义的变量 - } - if (!(child instanceof XLangVarScope scope)) { + PsiElement prev = node; + + // 从当前节点开始往前查找,并选择靠得最近的变量 + while (prev != null) { + prev = prev.getPrevSibling(); + if (!(prev instanceof XLangVarScope scope)) { continue; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java index de804d1dd..d84f67ac2 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java @@ -148,7 +148,6 @@ import org.jetbrains.annotations.NotNull; * @date 2025-07-03 */ public class StatementNode extends RuleSpecNode { - private VariableDeclarationNode[] varDeclaration; public StatementNode(@NotNull ASTNode node) { super(node); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java index bd466c343..924a764cd 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java @@ -61,7 +61,8 @@ public class VariableDeclaratorNode extends RuleSpecNode { String varName = identifier != null ? identifier.getText() : null; PsiClass varType = expression != null ? expression.getResultType() : null; - if (varName == null || varType == null) { + // Note: 变量类型可以是 null,表示未知类型 + if (varName == null) { return Map.of(); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiClassReference.java similarity index 88% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassReference.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiClassReference.java index 726f7a439..db67c73ce 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiClassReference.java @@ -11,10 +11,10 @@ import org.jetbrains.annotations.Nullable; * @author flytreeleft * @date 2025-07-04 */ -public class ClassReference extends PsiReferenceBase { +public class PsiClassReference extends PsiReferenceBase { private final PsiClass clazz; - public ClassReference( + public PsiClassReference( @NotNull PsiElement element, PsiClass clazz, TextRange rangeInElement ) { super(element, rangeInElement); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiFieldReference.java similarity index 79% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiFieldReference.java index 254bd2e65..1456be8af 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassPropertyReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiFieldReference.java @@ -13,10 +13,10 @@ import org.jetbrains.annotations.Nullable; * @author flytreeleft * @date 2025-07-01 */ -public class ClassPropertyReference extends PsiReferenceBase { +public class PsiFieldReference extends PsiReferenceBase { private final PsiField prop; - public ClassPropertyReference(@NotNull PsiElement element, PsiField prop, TextRange rangeInElement) { + public PsiFieldReference(@NotNull PsiElement element, PsiField prop, TextRange rangeInElement) { super(element, rangeInElement); this.prop = prop; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiMethodReference.java similarity index 80% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiMethodReference.java index e50c7327c..646f45953 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ClassMethodReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiMethodReference.java @@ -13,10 +13,10 @@ import org.jetbrains.annotations.Nullable; * @author flytreeleft * @date 2025-07-01 */ -public class ClassMethodReference extends PsiReferenceBase { +public class PsiMethodReference extends PsiReferenceBase { private final PsiMethod method; - public ClassMethodReference(@NotNull PsiElement element, PsiMethod method, TextRange rangeInElement) { + public PsiMethodReference(@NotNull PsiElement element, PsiMethod method, TextRange rangeInElement) { super(element, rangeInElement); this.method = method; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java index 2e031fa9c..69efbc534 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -23,13 +23,17 @@ import com.intellij.psi.PsiLiteralExpression; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifier; import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiPackage; import com.intellij.psi.PsiPrimitiveType; +import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceExpression; import com.intellij.psi.PsiReturnStatement; import com.intellij.psi.PsiType; import com.intellij.psi.PsiTypeParameter; import com.intellij.psi.PsiTypes; import com.intellij.psi.PsiWildcardType; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceProvider; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceSet; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.searches.ClassInheritorsSearch; import com.intellij.psi.util.PsiUtil; @@ -44,6 +48,8 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-26 */ public class PsiClassHelper { + private static final JavaClassReferenceProvider javaClassRefProvider = new JavaClassReferenceProvider(); + private static final Map primitiveTypeWrapper = Map.of(PsiTypes.byteType(), "java.lang.Byte", PsiTypes.shortType(), @@ -63,6 +69,23 @@ public class PsiClassHelper { PsiTypes.voidType(), "java.lang.Void"); + static { + // 支持解析包名:JavaClassReference#advancedResolveInner + javaClassRefProvider.setOption(JavaClassReferenceProvider.ADVANCED_RESOLVE, true); + } + + public static PsiReference @NotNull [] createJavaClassReferences( + String qualifiedName, PsiElement element, int startInElement + ) { + JavaClassReferenceSet refSet = new JavaClassReferenceSet(qualifiedName, + element, + startInElement, + false, + javaClassRefProvider); + + return refSet.getReferences(); + } + /** 得到 {@link PsiType} 对应的 {@link PsiClass} */ public static PsiClass getTypeClass(Project project, PsiType type) { if (type == null) { @@ -101,18 +124,18 @@ public class PsiClassHelper { } // 处理类类型(包括泛型) else if (type instanceof PsiClassType t) { - PsiClass cls = t.resolve(); + PsiClass clazz = t.resolve(); // 泛型参数 PsiType[] parameters = t.getParameters(); - if (cls != null && parameters.length > 0) { + if (clazz != null && parameters.length > 0) { // List -> 返回 String.class - if (CommonClassNames.JAVA_UTIL_LIST.equals(cls.getQualifiedName())) { + if (CommonClassNames.JAVA_UTIL_LIST.equals(clazz.getQualifiedName())) { return getTypeClass(project, parameters[0]); } // 自定义泛型类 - PsiTypeParameter[] typeParams = cls.getTypeParameters(); + PsiTypeParameter[] typeParams = clazz.getTypeParameters(); if (typeParams.length > 0) { // 查找实际使用的类型参数 for (int i = 0; i < typeParams.length; i++) { @@ -126,7 +149,7 @@ public class PsiClassHelper { } } } - return cls; + return clazz; } return null; @@ -143,39 +166,43 @@ public class PsiClassHelper { Map> map = new HashMap<>(); Query query = findInheritors(project, IStdDomainHandler.class.getName()); - query.filtering((cls) -> !cls.isInterface() - && !cls.isEnum() - && !cls.isAnnotationType() - && !cls.hasModifierProperty(PsiModifier.ABSTRACT) // + query.filtering((clazz) -> !clazz.isInterface() + && !clazz.isEnum() + && !clazz.isAnnotationType() + && !clazz.hasModifierProperty(PsiModifier.ABSTRACT) // ) // - .forEach((cls) -> { - Object name = getMethodReturnConstantValue(cls, "getName"); + .forEach((clazz) -> { + Object name = getMethodReturnConstantValue(clazz, "getName"); if (name != null) { - map.computeIfAbsent(name.toString(), (k) -> new ArrayList<>()).add(cls); + map.computeIfAbsent(name.toString(), (k) -> new ArrayList<>()).add(clazz); } }); return map; } - public static PsiClass findClass(Project project, String clsName) { - return JavaPsiFacade.getInstance(project).findClass(clsName, GlobalSearchScope.allScope(project)); + public static PsiClass findClass(Project project, String className) { + return JavaPsiFacade.getInstance(project).findClass(className, GlobalSearchScope.allScope(project)); + } + + public static PsiPackage findPackage(Project project, String pkgName) { + return JavaPsiFacade.getInstance(project).findPackage(pkgName); } /** 查找指定类的继承类 */ - public static @NotNull Query findInheritors(Project project, String clsName) { - PsiClass cls = findClass(project, clsName); - if (cls == null) { + public static @NotNull Query findInheritors(Project project, String className) { + PsiClass clazz = findClass(project, className); + if (clazz == null) { return EmptyQuery.getEmptyQuery(); } - return ClassInheritorsSearch.search(cls, true); + return ClassInheritorsSearch.search(clazz, true); } /** 获取指定方法返回的常量值 */ - public static Object getMethodReturnConstantValue(PsiClass cls, String methodName) { - PsiMethod[] methods = cls.findMethodsByName(methodName, true); + public static Object getMethodReturnConstantValue(PsiClass clazz, String methodName) { + PsiMethod[] methods = clazz.findMethodsByName(methodName, true); if (methods.length == 0) { return null; } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index e9697c3ac..1d34b5997 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -29,6 +29,8 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { a.b.c(1, 2); // let abc = new String("abc"); + let abc = new Abc.Def("abc"); + let abc = new java.lang.String("abc"); let def = new String("def").trim(); let def = 123, lmn = 456 + abc; const c = a.b.c; diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java index 35eb57639..aa8098101 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -15,6 +15,7 @@ import com.intellij.psi.PsiPackage; import com.intellij.psi.PsiReference; import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.idea.plugin.lang.script.XLangScriptFileType; +import io.nop.idea.plugin.lang.script.psi.IdentifierNode; /** * @author flytreeleft @@ -38,7 +39,20 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { /** 测试对对象成员的引用 */ public void testObjectMemberReference() { assertReference(""" + let a = b; "abc".trim(); + let a = b; + """, "java.lang.String#trim"); + assertReference(""" + let abc = 123; + abc.byteValue(); + let a = b; + """, "java.lang.Integer#byteValue"); + assertReference(""" + let abc = 123; + abc = "abc"; + abc.trim(); + let a = b; """, "java.lang.String#trim"); assertReference(""" @@ -48,7 +62,6 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { // 尝试触发无限递归 let name = handler.getName(); """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); - assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler(); @@ -56,14 +69,12 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { // 尝试触发无限递归 let name = handler.getName(); """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); - assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = XJsonDomainHandler.INSTANCE; // 尝试触发无限递归 let name = handler.getName(); """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#INSTANCE"); - assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; let name = XJsonDomainHandler.INSTANCE.getName(); @@ -77,30 +88,86 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { assertReference(""" let abc = "abc"; const def = abc + "def"; - """, ""); - } + abc = 123; + """, "@4"); + assertReference(""" + let abc = "abc"; + abc.trim(); + """, "@4"); - public void testJavaClassMemberReference() { - assertJavaReference(""" - public class Sample { - public static void main(String[] args) { - String a = " abc ".trim(); - } - } - """, ""); - } + assertReference(""" + const def = [1, 2, 3]; + def[0] = 2; + """, "@6"); - /** 通过在 text 中插入 <caret> 代表光标位置 */ - private void assertReference(String text, String expected) { - assertReference("sample." + ext, text, expected); - } + assertReference(""" + const s = new String("abc"); + """, "java.lang.String"); + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const handler = new XJsonDomainHandler(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + assertReference(""" + const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const sub = new XJsonDomainHandler.Sub.Sub(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub"); + assertReference(""" + const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub"); - private void assertJavaReference(String text, String expected) { - assertReference("Sample.java", text, expected); + assertReference(""" + const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler(); + handler.getName(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const sub = new XJsonDomainHandler.Sub.Sub(); + sub.getName(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName"); + assertReference(""" + const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); + sub.getName(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName"); + +// assertReference(""" +// function fn1(a, b) { return a + b; } +// fn1(1, 2); +// """, "@9"); +// assertReference(""" +// function fn1(a1, b1) { +// return a1 + b1; +// } +// """, ""); +// assertReference(""" +// function fn1(a1, b1) { +// return a1 + b1; +// } +// """, ""); + + assertReference(""" + const a1 = 'a'; + const b1 = 1; + const obj = {a1, b1: b1}; + """, "@6"); + assertReference(""" + const a1 = 'a'; + const b1 = 1; + const obj = {a1, b1: b1}; + """, "@22"); + assertReference(""" + const a1 = 'a'; + const b1 = 1; + const c1 = 'c'; + const obj = {a1, c1}; + """, "@36"); } - private void assertReference(String fileName, String text, String expected) { - myFixture.configureByText(fileName, text); + /** 通过在 text 中插入 <caret> 代表光标位置 */ + private void assertReference(String text, String expected) { + myFixture.configureByText("sample." + ext, text); PsiReference ref = findReferenceAtCaret(); assertNotNull(ref); @@ -124,6 +191,9 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { String actual = pkg.getQualifiedName(); assertEquals(expected, actual); } // + else if (target instanceof IdentifierNode id) { + assertEquals(expected, "@" + id.getTextOffset()); + } // else { fail("Unknown target " + target); } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast index 563b739f4..5757e58ac 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast @@ -7,17 +7,17 @@ FILE PsiElement('import')('import') PsiWhiteSpace(' ') ImportSourceNode(ast_importSource) - RuleSpecNode(qualifiedName) + QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) PsiElement(Identifier)('java') PsiElement('.')('.') - RuleSpecNode(qualifiedName) + QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) PsiElement(Identifier)('lang') PsiElement('.')('.') - RuleSpecNode(qualifiedName) + QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) PsiElement(Identifier)('String') @@ -30,17 +30,17 @@ FILE PsiElement('import')('import') PsiWhiteSpace(' ') ImportSourceNode(ast_importSource) - RuleSpecNode(qualifiedName) + QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) PsiElement(Identifier)('java') PsiElement('.')('.') - RuleSpecNode(qualifiedName) + QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) PsiElement(Identifier)('lang') PsiElement('.')('.') - RuleSpecNode(qualifiedName) + QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) PsiElement(Identifier)('Number') @@ -170,7 +170,7 @@ FILE PsiWhiteSpace(' ') ParameterizedTypeNode(parameterizedTypeNode) RuleSpecNode(qualifiedName_) - RuleSpecNode(qualifiedName) + QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) PsiElement(Identifier)('String') @@ -184,6 +184,89 @@ FILE RuleSpecNode(eos__) PsiElement(';')(';') PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + PsiElement('new')('new') + PsiWhiteSpace(' ') + ParameterizedTypeNode(parameterizedTypeNode) + RuleSpecNode(qualifiedName_) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('Abc') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('Def') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_string) + PsiElement(StringLiteral)('"abc"') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + PsiElement('new')('new') + PsiWhiteSpace(' ') + ParameterizedTypeNode(parameterizedTypeNode) + RuleSpecNode(qualifiedName_) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('java') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('lang') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('String') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_string) + PsiElement(StringLiteral)('"abc"') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') TopLevelStatementNode(ast_topLevelStatement) StatementNode(statement) VariableDeclarationNode(variableDeclaration) @@ -206,7 +289,7 @@ FILE PsiWhiteSpace(' ') ParameterizedTypeNode(parameterizedTypeNode) RuleSpecNode(qualifiedName_) - RuleSpecNode(qualifiedName) + QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) PsiElement(Identifier)('String') diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java index 5a0fc289a..fd52a23c0 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java @@ -13,4 +13,19 @@ public class XJsonDomainHandler implements IStdDomainHandler { public XJsonDomainHandler instance() { return INSTANCE; } + + public static class Sub { + + public static class Sub { + private String name; + + public SubSub(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + } } -- Gitee From 8caea01fecb90a15d530953e3416ffe2c8f6dc82 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sat, 5 Jul 2025 18:09:09 +0800 Subject: [PATCH 35/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20XLang=20Script=20=E5=86=85=E9=83=A8=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E5=87=BD=E6=95=B0=E5=92=8C=E7=AE=AD=E5=A4=B4=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E7=9A=84=E5=8F=82=E6=95=B0=E5=8F=8A=E5=85=B6=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../script/XLangScriptParserDefinition.java | 3 + .../lang/script/XLangScriptTokenTypes.java | 2 + .../lang/script/psi/ArrowFunctionNode.java | 32 +++++++ .../lang/script/psi/ExpressionNode.java | 2 +- .../script/psi/FunctionDeclarationNode.java | 13 +-- .../psi/FunctionParameterDeclarationNode.java | 78 +++++++++++++++++ .../script/psi/FunctionParameterListNode.java | 81 ++++++++++++++++++ .../lang/script/psi/IdentifierNode.java | 4 + .../lang/script/psi/ImportSourceNode.java | 2 +- .../script/psi/ObjectDeclarationNode.java | 2 +- .../script/psi/ParameterizedTypeNode.java | 12 +-- .../lang/script/psi/PsiClassAndTextRange.java | 28 +++++++ .../nop/idea/plugin/utils/PsiClassHelper.java | 2 +- .../lang/TestXLangScriptReferences.java | 84 +++++++++++++++---- 14 files changed, 305 insertions(+), 40 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/PsiClassAndTextRange.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index 1533ca1d8..a5f0dd03a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -19,6 +19,7 @@ import io.nop.idea.plugin.lang.script.psi.CalleeArgumentsNode; import io.nop.idea.plugin.lang.script.psi.ExpressionNode; import io.nop.idea.plugin.lang.script.psi.FunctionDeclarationNode; import io.nop.idea.plugin.lang.script.psi.FunctionParameterDeclarationNode; +import io.nop.idea.plugin.lang.script.psi.FunctionParameterListNode; import io.nop.idea.plugin.lang.script.psi.IdentifierNode; import io.nop.idea.plugin.lang.script.psi.ImportDeclarationNode; import io.nop.idea.plugin.lang.script.psi.ImportSourceNode; @@ -123,6 +124,8 @@ public class XLangScriptParserDefinition implements ParserDefinition { // case XLangParser.RULE_functionDeclaration -> // new FunctionDeclarationNode(node); + case XLangParser.RULE_parameterList_ -> // + new FunctionParameterListNode(node); case XLangParser.RULE_parameterDeclaration -> // new FunctionParameterDeclarationNode(node); case XLangParser.RULE_arrowFunctionExpression -> // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java index 4d9da1ac2..7c79d0d44 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java @@ -45,6 +45,8 @@ public class XLangScriptTokenTypes { public static final RuleIElementType RULE_moduleDeclaration_import = rule(XLangParser.RULE_moduleDeclaration_import); public static final RuleIElementType RULE_objectProperties = rule(XLangParser.RULE_objectProperties_); + public static final RuleIElementType RULE_namedTypeNode_annotation + = rule(XLangParser.RULE_namedTypeNode_annotation); public static TokenSet tokenSet(int... types) { return PSIElementTypeFactory.createTokenSet(XLangScriptLanguage.INSTANCE, types); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java index f487f608b..063b50dfd 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java @@ -6,6 +6,38 @@ import org.jetbrains.annotations.NotNull; /** * 箭头函数节点 + *

+ * (a, b) => a + b: + *

+ * ArrowFunctionNode(arrowFunctionExpression)
+ *   PsiElement('(')('(')
+ *   FunctionParameterListNode(parameterList_)
+ *     FunctionParameterDeclarationNode(parameterDeclaration)
+ *       RuleSpecNode(ast_identifierOrPattern)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('a')
+ *     PsiElement(',')(',')
+ *     PsiWhiteSpace(' ')
+ *     FunctionParameterDeclarationNode(parameterDeclaration)
+ *       RuleSpecNode(ast_identifierOrPattern)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('b')
+ *   PsiElement(')')(')')
+ *   PsiWhiteSpace(' ')
+ *   PsiElement('=>')('=>')
+ *   PsiWhiteSpace(' ')
+ *   ArrowFunctionBodyNode(expression_functionBody)
+ *     ExpressionNode(expression_single)
+ *       ExpressionNode(expression_single)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('a')
+ *       PsiWhiteSpace(' ')
+ *       PsiElement('+')('+')
+ *       PsiWhiteSpace(' ')
+ *       ExpressionNode(expression_single)
+ *         IdentifierNode(identifier)
+ *           PsiElement(Identifier)('b')
+ * 
* * @author flytreeleft * @date 2025-06-30 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index add9dff5d..c8d3a9ee8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -277,7 +277,7 @@ public class ExpressionNode extends RuleSpecNode { } @Override - public PsiReference @NotNull [] doGetReferences() { + protected PsiReference @NotNull [] doGetReferences() { // Note: // - 仅识别当前表达式的最后一个有效元素的引用,其余部分,由其子表达式做识别处理 // - 对象声明节点 ObjectDeclarationNode 的相关引用,由其自身负责构造 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java index 7330bcc43..7cab36189 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java @@ -1,10 +1,7 @@ package io.nop.idea.plugin.lang.script.psi; -import java.util.Map; - import com.intellij.lang.ASTNode; import com.intellij.psi.PsiClass; -import io.nop.idea.plugin.lang.XLangVarDecl; import org.jetbrains.annotations.NotNull; /** @@ -18,7 +15,7 @@ import org.jetbrains.annotations.NotNull; * IdentifierNode(identifier) * PsiElement(Identifier)('fn') * PsiElement('(')('(') - * RuleSpecNode(parameterList_) + * FunctionParameterListNode(parameterList_) * FunctionParameterDeclarationNode(parameterDeclaration) * RuleSpecNode(ast_identifierOrPattern) * IdentifierNode(identifier) @@ -74,12 +71,4 @@ public class FunctionDeclarationNode extends RuleSpecNode { // TODO 分析函数的 return 表达式,得到返回类型 return null; } - - /** 参数列表为函数内可访问的变量 */ - @Override - public @NotNull Map getVars() { - // TODO 分析参数列表,得到参数变量及其类型 - - return Map.of(); - } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java index 477193f4e..9aceccc9d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java @@ -1,17 +1,95 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiReference; +import io.nop.api.core.util.Symbol; +import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_namedTypeNode_annotation; + /** * 函数参数定义节点 + *

+ *

+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ *   RuleSpecNode(ast_identifierOrPattern)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('a')
+ * 
+ * + *
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ *   RuleSpecNode(ast_identifierOrPattern)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('a')
+ *   RuleSpecNode(namedTypeNode_annotation)
+ *     PsiElement(':')(':')
+ *     PsiWhiteSpace(' ')
+ *     RuleSpecNode(namedTypeNode)
+ *       RuleSpecNode(typeNameNode_predefined)
+ *         PsiElement('string')('string')
+ * 
* * @author flytreeleft * @date 2025-06-30 */ public class FunctionParameterDeclarationNode extends RuleSpecNode { + private IdentifierNode parameterName; public FunctionParameterDeclarationNode(@NotNull ASTNode node) { super(node); } + + public IdentifierNode getParameterName() { + if (parameterName == null || !parameterName.isValid()) { + parameterName = (IdentifierNode) getFirstChild().getFirstChild(); + } + return parameterName; + } + + public PsiClass getParameterType() { + PsiClassAndTextRange result = getClassAndTextRanges(); + + return result == null ? null : result.clazz(); + } + + @Override + protected PsiReference @NotNull [] doGetReferences() { + PsiClassAndTextRange result = getClassAndTextRanges(); + if (result == null || result.clazz() == null) { + return PsiReference.EMPTY_ARRAY; + } + + return new PsiReference[] { + PsiClassAndTextRange.createReference(this, result) + }; + } + + protected PsiClassAndTextRange getClassAndTextRanges() { + RuleSpecNode typeNode = findChildByType(RULE_namedTypeNode_annotation); + if (typeNode == null) { + return null; + } + + // 仅包含确定类型,详见 nop-xlang/model/antlr/XLangTypeSystem.g4 + RuleSpecNode typeNameNode = (RuleSpecNode) typeNode.getLastChild(); + String typeName = typeNameNode.getText(); + + Class typeClass = switch (typeName) { + case "any" -> Object.class; + case "number" -> Number.class; + case "boolean" -> Boolean.class; + case "string" -> String.class; + case "symbol" -> Symbol.class; + default -> null; + }; + + PsiClass clazz = typeClass != null ? PsiClassHelper.findClass(getProject(), typeClass.getName()) : null; + TextRange textRange = typeNameNode.getTextRangeInParent().shiftRight(typeNode.getStartOffsetInParent()); + + return new PsiClassAndTextRange(clazz, textRange); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java new file mode 100644 index 000000000..89cf548bd --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java @@ -0,0 +1,81 @@ +package io.nop.idea.plugin.lang.script.psi; + +import java.util.HashMap; +import java.util.Map; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import io.nop.idea.plugin.lang.XLangVarDecl; +import org.jetbrains.annotations.NotNull; + +/** + * 函数(含箭头函数)参数列表 + *

+ * 参数类型未知 a, b: + *

+ * FunctionParameterListNode(parameterList_)
+ *   FunctionParameterDeclarationNode(parameterDeclaration)
+ *     RuleSpecNode(ast_identifierOrPattern)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('a')
+ *   PsiElement(',')(',')
+ *   PsiWhiteSpace(' ')
+ *   FunctionParameterDeclarationNode(parameterDeclaration)
+ *     RuleSpecNode(ast_identifierOrPattern)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('b')
+ * 
+ * + * 含参数类型 a: string, b: number: + *
+ * FunctionParameterListNode(parameterList_)
+ *   FunctionParameterDeclarationNode(parameterDeclaration)
+ *     RuleSpecNode(ast_identifierOrPattern)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('a')
+ *     RuleSpecNode(namedTypeNode_annotation)
+ *       PsiElement(':')(':')
+ *       PsiWhiteSpace(' ')
+ *       RuleSpecNode(namedTypeNode)
+ *         RuleSpecNode(typeNameNode_predefined)
+ *           PsiElement('string')('string')
+ *   PsiElement(',')(',')
+ *   PsiWhiteSpace(' ')
+ *   FunctionParameterDeclarationNode(parameterDeclaration)
+ *     RuleSpecNode(ast_identifierOrPattern)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('b')
+ *     RuleSpecNode(namedTypeNode_annotation)
+ *       PsiElement(':')(':')
+ *       PsiWhiteSpace(' ')
+ *       RuleSpecNode(namedTypeNode)
+ *         RuleSpecNode(typeNameNode_predefined)
+ *           PsiElement('number')('number')
+ * 
+ * + * @author flytreeleft + * @date 2025-07-05 + */ +public class FunctionParameterListNode extends RuleSpecNode { + // Note: 在 FunctionParameterDeclarationNode 中构造对参数类型的引用 + + public FunctionParameterListNode(@NotNull ASTNode node) { + super(node); + } + + /** 参数列表为函数内可访问的变量 */ + @Override + public @NotNull Map getVars() { + FunctionParameterDeclarationNode[] paramDecls = findChildrenByClass(FunctionParameterDeclarationNode.class); + + Map vars = new HashMap<>(); + for (FunctionParameterDeclarationNode paramDecl : paramDecls) { + IdentifierNode paramName = paramDecl.getParameterName(); + PsiClass paramType = paramDecl.getParameterType(); + + vars.put(paramName.getText(), new XLangVarDecl(paramType, paramName)); + } + + return vars; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java index 9aed09485..a57b4e140 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java @@ -32,6 +32,10 @@ public class IdentifierNode extends RuleSpecNode { public PsiReference @NotNull [] createReferences(PsiElement source, TextRange textRange) { XLangVarDecl varDecl = getVarDecl(); + if (varDecl == null) { + return PsiReference.EMPTY_ARRAY; + } + PsiElement element = varDecl.element(); if (element instanceof IdentifierNode id) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java index b786cf271..03b84eb7b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -50,6 +50,6 @@ public class ImportSourceNode extends RuleSpecNode { QualifiedNameNode qnn = getQualifiedName(); String fqn = qnn.getFullyName(); - return PsiClassHelper.createJavaClassReferences(fqn, this, 0); + return PsiClassHelper.createJavaClassReferences(this, fqn, 0); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java index 6233cf5cf..fc057e736 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java @@ -54,7 +54,7 @@ public class ObjectDeclarationNode extends RuleSpecNode { } @Override - public PsiReference @NotNull [] doGetReferences() { + protected PsiReference @NotNull [] doGetReferences() { RuleSpecNode props = findChildByType(RULE_objectProperties); if (props == null) { return PsiReference.EMPTY_ARRAY; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java index d44474c35..949142a0e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java @@ -4,11 +4,9 @@ import java.util.ArrayList; import java.util.List; import com.intellij.lang.ASTNode; -import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; -import io.nop.idea.plugin.lang.script.reference.PsiClassReference; import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; @@ -65,12 +63,10 @@ public class ParameterizedTypeNode extends RuleSpecNode { if (result.isEmpty()) { String fqn = getText(); - return PsiClassHelper.createJavaClassReferences(fqn, this, 0); + return PsiClassHelper.createJavaClassReferences(this, fqn, 0); } - return result.stream() // - .map(r -> new PsiClassReference(this, r.clazz, r.textRange)) // - .toArray(PsiReference[]::new); + return PsiClassAndTextRange.createReferences(this, result); } public PsiClass getParameterizedType() { @@ -82,7 +78,7 @@ public class ParameterizedTypeNode extends RuleSpecNode { return PsiClassHelper.findClass(getProject(), fqn); } - PsiClass clazz = result.get(result.size() - 1).clazz; + PsiClass clazz = result.get(result.size() - 1).clazz(); String clazzName = clazz.getQualifiedName(); return clazzName != null // @@ -122,6 +118,4 @@ public class ParameterizedTypeNode extends RuleSpecNode { findInnerClass(subClazz, subQnn, qnn.getStartOffsetInParent(), result); } - - public record PsiClassAndTextRange(PsiClass clazz, TextRange textRange) {} } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/PsiClassAndTextRange.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/PsiClassAndTextRange.java new file mode 100644 index 000000000..f0ca03069 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/PsiClassAndTextRange.java @@ -0,0 +1,28 @@ +package io.nop.idea.plugin.lang.script.psi; + +import java.util.Collection; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import io.nop.idea.plugin.lang.script.reference.PsiClassReference; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-07-05 + */ +public record PsiClassAndTextRange(PsiClass clazz, TextRange textRange) { + + public static PsiReference @NotNull [] createReferences(PsiElement element, Collection list) { + return list.stream() + .filter(e -> e.clazz != null) + .map(e -> createReference(element, e)) + .toArray(PsiReference[]::new); + } + + public static @NotNull PsiReference createReference(PsiElement element, PsiClassAndTextRange cat) { + return new PsiClassReference(element, cat.clazz, cat.textRange); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java index 69efbc534..169e85a49 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -75,7 +75,7 @@ public class PsiClassHelper { } public static PsiReference @NotNull [] createJavaClassReferences( - String qualifiedName, PsiElement element, int startInElement + PsiElement element, String qualifiedName, int startInElement ) { JavaClassReferenceSet refSet = new JavaClassReferenceSet(qualifiedName, element, diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java index aa8098101..899362d8d 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -132,21 +132,6 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { sub.getName(); """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName"); -// assertReference(""" -// function fn1(a, b) { return a + b; } -// fn1(1, 2); -// """, "@9"); -// assertReference(""" -// function fn1(a1, b1) { -// return a1 + b1; -// } -// """, ""); -// assertReference(""" -// function fn1(a1, b1) { -// return a1 + b1; -// } -// """, ""); - assertReference(""" const a1 = 'a'; const b1 = 1; @@ -165,6 +150,75 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { """, "@36"); } + public void testFunctionReference() { + assertReference(""" + function fn1(a, b) { return a + b; } + fn1(1, 2); + """, "@9"); + assertReference(""" + function fn1(a, b) { return a + b; } + const a = fn1(1, 2); + """, "@9"); + assertReference(""" + const fn1 = (a1, b1) => a1 + b1; + fn1(1, 2); + """, "@6"); + assertReference(""" + const fn1 = (a1, b1) => a1 + b1; + const a = fn1(1, 2); + """, "@6"); + + // 对函数参数的引用 + assertReference(""" + function fn1(a1, b1) { + return a1 + b1; + } + """, "@13"); + assertReference(""" + function fn1(a1, b1) { + return a1 + b1; + } + """, "@17"); + assertReference(""" + const fn1 = (a1, b1) => a1 + b1; + """, "@13"); + assertReference(""" + const fn1 = (a1, b1) => a1 + b1; + """, "@17"); + + // 对函数参数类型的引用 + assertReference(""" + function fn1(a1: string, b1: number) { + return a1 + b1; + } + """, "java.lang.String"); + assertReference(""" + function fn1(a1: string, b1: number) { + return a1 + b1; + } + """, "java.lang.Number"); + assertReference(""" + const fn1 = (a1: string, b1: number) => a1 + b1; + """, "java.lang.String"); + assertReference(""" + const fn1 = (a1: string, b1: number) => a1 + b1; + """, "java.lang.Number"); + +// // TODO 对函数调用的结果类型的引用 +// assertReference(""" +// function fn1(a, b) { return a + b; } +// const a = fn1('a', 'b'); +// a.trim(); +// """, "java.lang.String#trim"); +// assertReference(""" +// function fn1(a, b) { return a + b; } +// const a = fn1('a', 'b'); +// a.trim(); +// const b = fn1(1, 2); +// b.byteValue(); +// """, "java.lang.Integer#byteValue"); + } + /** 通过在 text 中插入 <caret> 代表光标位置 */ private void assertReference(String text, String expected) { myFixture.configureByText("sample." + ext, text); -- Gitee From 735f5bb42cbd2934bd2e2a32707e83403a563a4c Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sat, 5 Jul 2025 21:25:11 +0800 Subject: [PATCH 36/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B=20XL?= =?UTF-8?q?ang=20Script=20=E4=B8=AD=E7=B1=BB=E5=9E=8B=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E8=AF=86=E5=88=AB=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../script/XLangScriptParserDefinition.java | 18 +- .../lang/script/XLangScriptTokenTypes.java | 8 +- .../lang/script/psi/ExpressionNode.java | 7 +- .../psi/FunctionParameterDeclarationNode.java | 46 +---- ...peNode.java => QualifiedNameRootNode.java} | 36 ++-- .../psi/TypeNameNodePredefinedNode.java | 68 +++++++ .../plugin/lang/TestXLangScriptParser.java | 17 +- .../lang/TestXLangScriptReferences.java | 27 +++ .../resources/_vfs/test/ast/statement-1.ast | 171 ++++++++++++++++-- .../_vfs/test/ast/statement-err-1.ast | 96 +++++++--- .../_vfs/test/ast/statement-err-2.ast | 21 --- .../_vfs/test/ast/statement-err-3.ast | 26 --- .../_vfs/test/java/XJsonDomainHandler.java | 9 +- .../test/resources/_vfs/test/reference/a.xlib | 2 + 14 files changed, 386 insertions(+), 166 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/{ParameterizedTypeNode.java => QualifiedNameRootNode.java} (77%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java delete mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-2.ast delete mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-3.ast diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index a5f0dd03a..b74a198d1 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -28,17 +28,20 @@ import io.nop.idea.plugin.lang.script.psi.ObjectDeclarationNode; import io.nop.idea.plugin.lang.script.psi.ObjectMemberNode; import io.nop.idea.plugin.lang.script.psi.ObjectPropertyAssignmentNode; import io.nop.idea.plugin.lang.script.psi.ObjectPropertyDeclarationNode; -import io.nop.idea.plugin.lang.script.psi.ParameterizedTypeNode; import io.nop.idea.plugin.lang.script.psi.ProgramNode; import io.nop.idea.plugin.lang.script.psi.QualifiedNameNode; +import io.nop.idea.plugin.lang.script.psi.QualifiedNameRootNode; import io.nop.idea.plugin.lang.script.psi.ReturnStatementNode; import io.nop.idea.plugin.lang.script.psi.RuleSpecNode; import io.nop.idea.plugin.lang.script.psi.StatementNode; import io.nop.idea.plugin.lang.script.psi.TopLevelStatementNode; +import io.nop.idea.plugin.lang.script.psi.TypeNameNodePredefinedNode; import io.nop.idea.plugin.lang.script.psi.VariableDeclarationNode; import io.nop.idea.plugin.lang.script.psi.VariableDeclaratorNode; import io.nop.idea.plugin.lang.script.psi.VariableDeclaratorsNode; +import io.nop.xlang.parse.antlr.XLangLexer; import io.nop.xlang.parse.antlr.XLangParser; +import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; import org.antlr.intellij.adaptor.lexer.RuleIElementType; import org.jetbrains.annotations.NotNull; @@ -55,6 +58,13 @@ import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_whitesp public class XLangScriptParserDefinition implements ParserDefinition { public static final IFileElementType FILE = new IFileElementType(XLangScriptLanguage.INSTANCE); + // Note: 确保在插件加载时,完成对 PSIElementTypeFactory 的初始化 + static { + PSIElementTypeFactory.defineLanguageIElementTypes(XLangScriptLanguage.INSTANCE, + XLangLexer.tokenNames, + XLangParser.ruleNames); + } + @NotNull @Override public Lexer createLexer(Project project) { @@ -106,6 +116,8 @@ public class XLangScriptParserDefinition implements ParserDefinition { new AssignmentExpressionNode(node); case XLangParser.RULE_arrayExpression -> // new ArrayExpressionNode(node); + case XLangParser.RULE_typeNameNode_predefined -> // + new TypeNameNodePredefinedNode(node); // case XLangParser.RULE_expression_single -> // new ExpressionNode(node); @@ -113,8 +125,8 @@ public class XLangScriptParserDefinition implements ParserDefinition { new ObjectDeclarationNode(node); case XLangParser.RULE_ast_objectProperty -> // new ObjectPropertyDeclarationNode(node); - case XLangParser.RULE_parameterizedTypeNode -> // - new ParameterizedTypeNode(node); + case XLangParser.RULE_qualifiedName_ -> // + new QualifiedNameRootNode(node); case XLangParser.RULE_arguments_ -> // new CalleeArgumentsNode(node); case XLangParser.RULE_identifier_ex -> // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java index 7c79d0d44..34dcd2260 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java @@ -14,12 +14,7 @@ import org.antlr.intellij.adaptor.lexer.TokenIElementType; * @date 2025-07-04 */ public class XLangScriptTokenTypes { - static { - PSIElementTypeFactory.defineLanguageIElementTypes(XLangScriptLanguage.INSTANCE, - XLangLexer.tokenNames, - XLangParser.ruleNames); - } - + // Note: 需在 XLangScriptParserDefinition 中完成对 PSIElementTypeFactory 的初始化 public static final List TOKEN_ELEMENT_TYPES = // PSIElementTypeFactory.getTokenIElementTypes(XLangScriptLanguage.INSTANCE); public static final List RULE_ELEMENT_TYPES = // @@ -47,6 +42,7 @@ public class XLangScriptTokenTypes { public static final RuleIElementType RULE_objectProperties = rule(XLangParser.RULE_objectProperties_); public static final RuleIElementType RULE_namedTypeNode_annotation = rule(XLangParser.RULE_namedTypeNode_annotation); + public static final RuleIElementType RULE_parameterizedTypeNode = rule(XLangParser.RULE_parameterizedTypeNode); public static TokenSet tokenSet(int... types) { return PSIElementTypeFactory.createTokenSet(XLangScriptLanguage.INSTANCE, types); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index c8d3a9ee8..62b72c975 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -16,6 +16,8 @@ import io.nop.idea.plugin.lang.script.reference.PsiFieldReference; import io.nop.idea.plugin.lang.script.reference.PsiMethodReference; import org.jetbrains.annotations.NotNull; +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_parameterizedTypeNode; + /** * 表达式节点 *

@@ -342,9 +344,10 @@ public class ExpressionNode extends RuleSpecNode { return i.getDataType(); } // else if (isObjectConstructorCall()) { - ParameterizedTypeNode cons = findChildByClass(ParameterizedTypeNode.class); + RuleSpecNode ptn = findChildByType(RULE_parameterizedTypeNode); + QualifiedNameRootNode cons = ptn != null ? (QualifiedNameRootNode) ptn.getFirstChild() : null; - return cons != null ? cons.getParameterizedType() : null; + return cons != null ? cons.getQualifiedType() : null; } // else if (isObjectMethodCall()) { PsiMethod method = getObjectMethod(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java index 9aceccc9d..0412dfd0c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java @@ -1,11 +1,7 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; -import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiReference; -import io.nop.api.core.util.Symbol; -import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_namedTypeNode_annotation; @@ -51,45 +47,19 @@ public class FunctionParameterDeclarationNode extends RuleSpecNode { } public PsiClass getParameterType() { - PsiClassAndTextRange result = getClassAndTextRanges(); - - return result == null ? null : result.clazz(); - } - - @Override - protected PsiReference @NotNull [] doGetReferences() { - PsiClassAndTextRange result = getClassAndTextRanges(); - if (result == null || result.clazz() == null) { - return PsiReference.EMPTY_ARRAY; - } - - return new PsiReference[] { - PsiClassAndTextRange.createReference(this, result) - }; - } - - protected PsiClassAndTextRange getClassAndTextRanges() { RuleSpecNode typeNode = findChildByType(RULE_namedTypeNode_annotation); if (typeNode == null) { return null; } - // 仅包含确定类型,详见 nop-xlang/model/antlr/XLangTypeSystem.g4 - RuleSpecNode typeNameNode = (RuleSpecNode) typeNode.getLastChild(); - String typeName = typeNameNode.getText(); - - Class typeClass = switch (typeName) { - case "any" -> Object.class; - case "number" -> Number.class; - case "boolean" -> Boolean.class; - case "string" -> String.class; - case "symbol" -> Symbol.class; - default -> null; - }; + RuleSpecNode type = (RuleSpecNode) typeNode.getLastChild().getFirstChild(); - PsiClass clazz = typeClass != null ? PsiClassHelper.findClass(getProject(), typeClass.getName()) : null; - TextRange textRange = typeNameNode.getTextRangeInParent().shiftRight(typeNode.getStartOffsetInParent()); - - return new PsiClassAndTextRange(clazz, textRange); + if (type instanceof TypeNameNodePredefinedNode tp) { + return tp.getPredefinedType(); + } // + else if (type instanceof QualifiedNameRootNode qnr) { + return qnr.getQualifiedType(); + } + return null; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java similarity index 77% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java index 949142a0e..3e66293ff 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ParameterizedTypeNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java @@ -11,46 +11,44 @@ import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; /** - * new 语句中的类名节点 + * 类名节点 *

* String: *

- * ParameterizedTypeNode(parameterizedTypeNode)
- *   RuleSpecNode(qualifiedName_)
- *     QualifiedNameNode(qualifiedName)
- *       RuleSpecNode(qualifiedName_name_)
- *         IdentifierNode(identifier)
- *           PsiElement(Identifier)('String')
+ * RuleSpecNode(qualifiedName_)
+ *   QualifiedNameNode(qualifiedName)
+ *     RuleSpecNode(qualifiedName_name_)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('String')
  * 
* * Abc.Def: *
- * ParameterizedTypeNode(parameterizedTypeNode)
- *   RuleSpecNode(qualifiedName_)
+ * RuleSpecNode(qualifiedName_)
+ *   QualifiedNameNode(qualifiedName)
+ *     RuleSpecNode(qualifiedName_name_)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('Abc')
+ *     PsiElement('.')('.')
  *     QualifiedNameNode(qualifiedName)
  *       RuleSpecNode(qualifiedName_name_)
  *         IdentifierNode(identifier)
- *           PsiElement(Identifier)('Abc')
- *       PsiElement('.')('.')
- *       QualifiedNameNode(qualifiedName)
- *         RuleSpecNode(qualifiedName_name_)
- *           IdentifierNode(identifier)
- *             PsiElement(Identifier)('Def')
+ *           PsiElement(Identifier)('Def')
  * 
* * @author flytreeleft * @date 2025-06-30 */ -public class ParameterizedTypeNode extends RuleSpecNode { +public class QualifiedNameRootNode extends RuleSpecNode { private QualifiedNameNode qualifiedName; - public ParameterizedTypeNode(@NotNull ASTNode node) { + public QualifiedNameRootNode(@NotNull ASTNode node) { super(node); } public QualifiedNameNode getQualifiedName() { if (qualifiedName == null || !qualifiedName.isValid()) { - qualifiedName = (QualifiedNameNode) getFirstChild().getFirstChild(); + qualifiedName = (QualifiedNameNode) getFirstChild(); } return qualifiedName; } @@ -69,7 +67,7 @@ public class ParameterizedTypeNode extends RuleSpecNode { return PsiClassAndTextRange.createReferences(this, result); } - public PsiClass getParameterizedType() { + public PsiClass getQualifiedType() { List result = getClassAndTextRanges(); String fqn = getText().replace(" ", ""); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java new file mode 100644 index 000000000..646f8bfda --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java @@ -0,0 +1,68 @@ +package io.nop.idea.plugin.lang.script.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import io.nop.api.core.util.Symbol; +import io.nop.idea.plugin.lang.script.reference.PsiClassReference; +import io.nop.idea.plugin.utils.PsiClassHelper; +import org.jetbrains.annotations.NotNull; + +/** + * 预定义类型节点 + *

+ *

+ * RuleSpecNode(typeNameNode_predefined)
+ *   PsiElement('string')('string')
+ * 
+ * + * @author flytreeleft + * @date 2025-07-05 + */ +public class TypeNameNodePredefinedNode extends RuleSpecNode { + private PsiElement typeName; + + public TypeNameNodePredefinedNode(@NotNull ASTNode node) { + super(node); + } + + public PsiElement getTypeName() { + if (typeName == null || !typeName.isValid()) { + typeName = getLastChild(); + } + return typeName; + } + + public PsiClass getPredefinedType() { + // 仅包含确定类型,详见 nop-xlang/model/antlr/XLangTypeSystem.g4 + PsiElement typeNameNode = getTypeName(); + String typeName = typeNameNode.getText(); + + Class typeClass = switch (typeName) { + case "any" -> Object.class; + case "number" -> Number.class; + case "boolean" -> Boolean.class; + case "string" -> String.class; + case "symbol" -> Symbol.class; + case "void" -> Void.class; + default -> null; + }; + + return typeClass != null ? PsiClassHelper.findClass(getProject(), typeClass.getName()) : null; + } + + @Override + protected PsiReference @NotNull [] doGetReferences() { + PsiClass clazz = getPredefinedType(); + if (clazz == null) { + return PsiReference.EMPTY_ARRAY; + } + + PsiElement typeNameNode = getTypeName(); + + return new PsiReference[] { + new PsiClassReference(this, clazz, typeNameNode.getTextRangeInParent()) + }; + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index 1d34b5997..9bc5381cc 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -15,13 +15,16 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); public void testParseStatement() { -// assertParseTree("import java.lang.;", "/test/ast/statement-err-1.ast"); -// assertParseTree("const abc = ", "/test/ast/statement-err-2.ast"); -// assertParseTree("const abc = () =>", "/test/ast/statement-err-3.ast"); -// + assertParseTree(""" + import java.lang.; + const abc = ; + const abc = () =>; + """, "/test/ast/statement-err-1.ast"); + assertParseTree(""" import java.lang.String; import java.lang.Number; + import io.nop.xlang.xdef.domain.XJsonDomainHandler; // const abc = ormTemplate.findListByQuery(query, mapper); // @@ -41,13 +44,15 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { let xyz; xyz = "234"; // + const b = s instanceof string; + // const fn1 = (a, b) => a + b; function fn2(a, b) { const c = 5; return a + b + c; } - function fn3(a: string, b: number) { - return a + b; + function fn3(a: string, b: number, c: XJsonDomainHandler, d: XJsonDomainHandler.Sub) { + return a + b + c.getName() + d.getName(); } // if (a > 2) { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java index 899362d8d..e69fd1a15 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -197,6 +197,26 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { return a1 + b1; } """, "java.lang.Number"); + + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + function fn1(a1: string, b1: XJsonDomainHandler) { + return a1 + b1; + } + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + function fn1(a1: string, b1: XJsonDomainHandler.Sub.Sub) { + return a1 + b1; + } + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub"); + assertReference(""" + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + function fn1(a1: XJsonDomainHandler.Sub.Sub) { + return a1.getName(); + } + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName"); + assertReference(""" const fn1 = (a1: string, b1: number) => a1 + b1; """, "java.lang.String"); @@ -204,6 +224,13 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { const fn1 = (a1: string, b1: number) => a1 + b1; """, "java.lang.Number"); + assertReference(""" + const b = s instanceof string; + """, "java.lang.String"); + assertReference(""" + const b = s instanceof number; + """, "java.lang.Number"); + // // TODO 对函数调用的结果类型的引用 // assertReference(""" // function fn1(a, b) { return a + b; } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast index 5757e58ac..55f352571 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast @@ -47,6 +47,44 @@ FILE RuleSpecNode(eos__) PsiElement(';')(';') PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + RuleSpecNode(moduleDeclaration_import) + ImportDeclarationNode(importAsDeclaration) + PsiElement('import')('import') + PsiWhiteSpace(' ') + ImportSourceNode(ast_importSource) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('io') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('nop') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('xlang') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('xdef') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('domain') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('XJsonDomainHandler') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') PsiElement(SingleLineComment)('//') PsiWhiteSpace('\n') TopLevelStatementNode(ast_topLevelStatement) @@ -168,8 +206,8 @@ FILE ExpressionNode(expression_single) PsiElement('new')('new') PsiWhiteSpace(' ') - ParameterizedTypeNode(parameterizedTypeNode) - RuleSpecNode(qualifiedName_) + RuleSpecNode(parameterizedTypeNode) + QualifiedNameRootNode(qualifiedName_) QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) @@ -202,8 +240,8 @@ FILE ExpressionNode(expression_single) PsiElement('new')('new') PsiWhiteSpace(' ') - ParameterizedTypeNode(parameterizedTypeNode) - RuleSpecNode(qualifiedName_) + RuleSpecNode(parameterizedTypeNode) + QualifiedNameRootNode(qualifiedName_) QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) @@ -241,8 +279,8 @@ FILE ExpressionNode(expression_single) PsiElement('new')('new') PsiWhiteSpace(' ') - ParameterizedTypeNode(parameterizedTypeNode) - RuleSpecNode(qualifiedName_) + RuleSpecNode(parameterizedTypeNode) + QualifiedNameRootNode(qualifiedName_) QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) @@ -287,8 +325,8 @@ FILE ExpressionNode(expression_single) PsiElement('new')('new') PsiWhiteSpace(' ') - ParameterizedTypeNode(parameterizedTypeNode) - RuleSpecNode(qualifiedName_) + RuleSpecNode(parameterizedTypeNode) + QualifiedNameRootNode(qualifiedName_) QualifiedNameNode(qualifiedName) RuleSpecNode(qualifiedName_name_) IdentifierNode(identifier) @@ -530,6 +568,36 @@ FILE PsiWhiteSpace('\n') PsiElement(SingleLineComment)('//') PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('s') + PsiWhiteSpace(' ') + PsiElement('instanceof')('instanceof') + PsiWhiteSpace(' ') + RuleSpecNode(namedTypeNode) + TypeNameNodePredefinedNode(typeNameNode_predefined) + PsiElement('string')('string') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + PsiElement(SingleLineComment)('//') + PsiWhiteSpace('\n') TopLevelStatementNode(ast_topLevelStatement) StatementNode(statement) VariableDeclarationNode(variableDeclaration) @@ -548,7 +616,7 @@ FILE ExpressionNode(expression_single) ArrowFunctionNode(arrowFunctionExpression) PsiElement('(')('(') - RuleSpecNode(parameterList_) + FunctionParameterListNode(parameterList_) FunctionParameterDeclarationNode(parameterDeclaration) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) @@ -585,7 +653,7 @@ FILE IdentifierNode(identifier) PsiElement(Identifier)('fn2') PsiElement('(')('(') - RuleSpecNode(parameterList_) + FunctionParameterListNode(parameterList_) FunctionParameterDeclarationNode(parameterDeclaration) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) @@ -659,7 +727,7 @@ FILE IdentifierNode(identifier) PsiElement(Identifier)('fn3') PsiElement('(')('(') - RuleSpecNode(parameterList_) + FunctionParameterListNode(parameterList_) FunctionParameterDeclarationNode(parameterDeclaration) RuleSpecNode(ast_identifierOrPattern) IdentifierNode(identifier) @@ -668,7 +736,7 @@ FILE PsiElement(':')(':') PsiWhiteSpace(' ') RuleSpecNode(namedTypeNode) - RuleSpecNode(typeNameNode_predefined) + TypeNameNodePredefinedNode(typeNameNode_predefined) PsiElement('string')('string') PsiElement(',')(',') PsiWhiteSpace(' ') @@ -680,8 +748,43 @@ FILE PsiElement(':')(':') PsiWhiteSpace(' ') RuleSpecNode(namedTypeNode) - RuleSpecNode(typeNameNode_predefined) + TypeNameNodePredefinedNode(typeNameNode_predefined) PsiElement('number')('number') + PsiElement(',')(',') + PsiWhiteSpace(' ') + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + RuleSpecNode(namedTypeNode_annotation) + PsiElement(':')(':') + PsiWhiteSpace(' ') + RuleSpecNode(namedTypeNode) + QualifiedNameRootNode(qualifiedName_) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('XJsonDomainHandler') + PsiElement(',')(',') + PsiWhiteSpace(' ') + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('d') + RuleSpecNode(namedTypeNode_annotation) + PsiElement(':')(':') + PsiWhiteSpace(' ') + RuleSpecNode(namedTypeNode) + QualifiedNameRootNode(qualifiedName_) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('XJsonDomainHandler') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('Sub') PsiElement(')')(')') PsiWhiteSpace(' ') BlockStatementNode(blockStatement) @@ -695,14 +798,48 @@ FILE PsiWhiteSpace(' ') ExpressionNode(expression_single) ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('a') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + PsiWhiteSpace(' ') + PsiElement('+')('+') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + PsiWhiteSpace(' ') + PsiElement('+')('+') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('c') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('getName') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + PsiElement(')')(')') PsiWhiteSpace(' ') PsiElement('+')('+') PsiWhiteSpace(' ') ExpressionNode(expression_single) - IdentifierNode(identifier) - PsiElement(Identifier)('b') + ExpressionNode(expression_single) + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('d') + PsiElement('.')('.') + ObjectMemberNode(identifier_ex) + RuleSpecNode(identifierOrKeyword_) + IdentifierNode(identifier) + PsiElement(Identifier)('getName') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + PsiElement(')')(')') RuleSpecNode(eos__) PsiElement(';')(';') PsiWhiteSpace('\n') diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast index 980195c60..ce1956fe1 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast @@ -1,25 +1,73 @@ -FILE(0,18) - ProgramNode(program)(0,18) - RuleSpecNode(topLevelStatements_)(0,18) - RuleSpecNode(ast_topLevelStatement)(0,17) - RuleSpecNode(moduleDeclaration_import)(0,17) - ImportAsDeclarationNode(importAsDeclaration)(0,17) - PsiElement('import')('import')(0,6) - ImportSourceNode(ast_importSource)(7,16) - ImportQualifiedNameNode(qualifiedName)(7,16) - RuleSpecNode(qualifiedName_name_)(7,11) - RuleSpecNode(identifier)(7,11) - PsiElement(Identifier)('java')(7,11) - PsiElement('.')('.')(11,12) - ImportQualifiedNameNode(qualifiedName)(12,16) - RuleSpecNode(qualifiedName_name_)(12,16) - RuleSpecNode(identifier)(12,16) - PsiElement(Identifier)('lang')(12,16) - RuleSpecNode(eos__)(16,17) +FILE + ProgramNode(program) + RuleSpecNode(topLevelStatements_) + TopLevelStatementNode(ast_topLevelStatement) + RuleSpecNode(moduleDeclaration_import) + ImportDeclarationNode(importAsDeclaration) + PsiElement('import')('import') + PsiWhiteSpace(' ') + ImportSourceNode(ast_importSource) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('java') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('lang') + RuleSpecNode(eos__) PsiErrorElement:rule eos__ failed predicate: {this.lineTerminatorAhead()}? -(16,17) - PsiElement('.')('.')(16,17) - RuleSpecNode(ast_topLevelStatement)(17,18) - RuleSpecNode(statement)(17,18) - RuleSpecNode(emptyStatement)(17,18) - PsiElement(';')(';')(17,18) + + PsiElement('.')('.') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + RuleSpecNode(emptyStatement) + PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('abc') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + ArrowFunctionNode(arrowFunctionExpression) + PsiElement('(')('(') + PsiElement(')')(')') + PsiWhiteSpace(' ') + PsiElement('=>')('=>') + ArrowFunctionBodyNode(expression_functionBody) + + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-2.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-2.ast deleted file mode 100644 index e86f49e99..000000000 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-2.ast +++ /dev/null @@ -1,21 +0,0 @@ -FILE(0,12) - ProgramNode(program)(0,12) - RuleSpecNode(topLevelStatements_)(0,12) - RuleSpecNode(ast_topLevelStatement)(0,12) - RuleSpecNode(statement)(0,12) - RuleSpecNode(variableDeclaration)(0,12) - RuleSpecNode(varModifier_)(0,5) - PsiElement('const')('const')(0,5) - RuleSpecNode(variableDeclarators_)(6,12) - RuleSpecNode(variableDeclarator)(6,12) - RuleSpecNode(ast_identifierOrPattern)(6,9) - RuleSpecNode(identifier)(6,9) - PsiElement(Identifier)('abc')(6,9) - RuleSpecNode(expression_initializer)(10,12) - PsiElement('=')('=')(10,11) - RuleSpecNode(expression_single)(12,12) - PsiErrorElement:mismatched input '' expecting {RegularExpressionLiteral, '[', '(', '{', '++', '--', '+', '-', '~', '!', 'null', BooleanLiteral, DecimalIntegerLiteral, HexIntegerLiteral, BinaryIntegerLiteral, DecimalLiteral, 'typeof', 'new', 'this', 'from', 'super', 'type', StringLiteral, TemplateStringLiteral, Identifier, '#{'} -(12,12) - - RuleSpecNode(eos__)(12,12) - diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-3.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-3.ast deleted file mode 100644 index 3185c842c..000000000 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-3.ast +++ /dev/null @@ -1,26 +0,0 @@ -FILE(0,17) - ProgramNode(program)(0,17) - RuleSpecNode(topLevelStatements_)(0,17) - RuleSpecNode(ast_topLevelStatement)(0,17) - RuleSpecNode(statement)(0,17) - RuleSpecNode(variableDeclaration)(0,17) - RuleSpecNode(varModifier_)(0,5) - PsiElement('const')('const')(0,5) - RuleSpecNode(variableDeclarators_)(6,17) - RuleSpecNode(variableDeclarator)(6,17) - RuleSpecNode(ast_identifierOrPattern)(6,9) - RuleSpecNode(identifier)(6,9) - PsiElement(Identifier)('abc')(6,9) - RuleSpecNode(expression_initializer)(10,17) - PsiElement('=')('=')(10,11) - RuleSpecNode(expression_single)(12,17) - RuleSpecNode(arrowFunctionExpression)(12,17) - PsiElement('(')('(')(12,13) - PsiElement(')')(')')(13,14) - PsiElement('=>')('=>')(15,17) - RuleSpecNode(expression_functionBody)(17,17) - PsiErrorElement:mismatched input '' expecting {RegularExpressionLiteral, '[', '(', '{', '++', '--', '+', '-', '~', '!', 'null', BooleanLiteral, DecimalIntegerLiteral, HexIntegerLiteral, BinaryIntegerLiteral, DecimalLiteral, 'typeof', 'new', 'this', 'from', 'super', 'type', StringLiteral, TemplateStringLiteral, Identifier, '#{'} -(17,17) - - RuleSpecNode(eos__)(17,17) - diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java index fd52a23c0..478d691df 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java @@ -15,14 +15,15 @@ public class XJsonDomainHandler implements IStdDomainHandler { } public static class Sub { + private String name; + + public String getName() { + return this.name; + } public static class Sub { private String name; - public SubSub(String name) { - this.name = name; - } - public String getName() { return this.name; } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index 22bf9a3a2..827735656 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -19,6 +19,8 @@ const ormTemplate = inject('nopOrmTemplate'); const mapper = ormTemplate.getRowMapper(rowType,false); const s = "abc".startsWith("a"); + const fn = (a: any, b: string) => a + b; + fn(1, 2); while (true) { let a = 's' instanceof string; -- Gitee From a4868984d5247e7208d3c00aa0826729d53dbf3e Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 6 Jul 2025 15:10:39 +0800 Subject: [PATCH 37/82] =?UTF-8?q?nop-idea-pugin:=20=E5=B0=86=20XLang=20Scr?= =?UTF-8?q?ipt=20=E4=B8=AD=E7=9A=84=E5=BC=95=E7=94=A8=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E5=85=83=E7=B4=A0=E7=9A=84=E6=9F=A5=E6=89=BE=E5=BB=B6=E8=BF=9F?= =?UTF-8?q?=E5=88=B0=20PsiReference#resolve=20=E8=B0=83=E7=94=A8=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=AF=B9=E7=BB=93=E6=9E=9C=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E7=BC=93=E5=AD=98=EF=BC=8C=E4=BB=8E=E8=80=8C=E9=99=8D=E4=BD=8E?= =?UTF-8?q?=E6=9F=A5=E6=89=BE=E9=80=BB=E8=BE=91=E5=AF=B9=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E7=9A=84=E5=BD=B1=E5=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/reference/XLangReferenceBase.java | 73 ++++++++++ .../lang/script/psi/ExpressionNode.java | 126 ++++++++++-------- .../psi/FunctionParameterDeclarationNode.java | 6 +- .../lang/script/psi/IdentifierNode.java | 34 +---- .../lang/script/psi/ImportSourceNode.java | 6 +- .../script/psi/ObjectDeclarationNode.java | 7 +- .../lang/script/psi/PsiClassAndTextRange.java | 28 ---- .../lang/script/psi/QualifiedNameNode.java | 6 +- .../script/psi/QualifiedNameRootNode.java | 71 +++++----- .../psi/TypeNameNodePredefinedNode.java | 39 ++---- .../script/psi/VariableDeclaratorNode.java | 41 ++++++ ...eference.java => IdentifierReference.java} | 37 +++-- .../reference/ObjectMethodReference.java | 42 ++++++ .../reference/ObjectPropertyReference.java | 44 ++++++ .../reference/PredefinedTypeReference.java | 63 +++++++++ .../script/reference/PsiClassReference.java | 33 ----- .../script/reference/PsiFieldReference.java | 33 ----- .../script/reference/PsiMethodReference.java | 33 ----- .../reference/QualifiedNameReference.java | 85 ++++++++++++ .../reference/XLangVfsFileReference.java | 9 -- .../plugin/lang/TestXLangScriptParser.java | 2 + .../lang/TestXLangScriptReferences.java | 35 ++++- .../resources/_vfs/test/ast/statement-1.ast | 49 +++++++ 23 files changed, 579 insertions(+), 323 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/PsiClassAndTextRange.java rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/{VariableDeclarationReference.java => IdentifierReference.java} (32%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectPropertyReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiClassReference.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiFieldReference.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiMethodReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java new file mode 100644 index 000000000..922c695c0 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java @@ -0,0 +1,73 @@ +package io.nop.idea.plugin.lang.reference; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.impl.source.resolve.reference.impl.CachingReference; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-07-06 + */ +public abstract class XLangReferenceBase extends CachingReference { + private static final Logger LOG = Logger.getInstance(XLangReferenceBase.class); + + protected final PsiElement myElement; + private TextRange myRangeInElement; + + /** + * 在元素 myElement 可以被拆分为多个相互间有关联的引用时, + * 所构造的各个引用均将从属于 myElement,而 + * myRangeInElement 则为当前引用在 myElement + * 中所对应的文本内容在该元素内所在的相对范围,如,在包 io.nop.core + * 中,nop 包的 {@link TextRange} 为 [3, 6] + *

+ * 而如果 myElement 仅有唯一的引用,则 + * myRangeInElement 为该元素的文本自身,即,[0, 文本长度] + *

+ * 相关处理逻辑见 {@link com.intellij.psi.impl.SharedPsiElementImplUtil#addReferences SharedPsiElementImplUtil#addReferences} + */ + public XLangReferenceBase(PsiElement myElement, TextRange myRangeInElement) { + this.myElement = myElement; + this.myRangeInElement = myRangeInElement; + } + + @NotNull + @Override + public PsiElement getElement() { + return myElement; + } + + @NotNull + @Override + public TextRange getRangeInElement() { + TextRange rangeInElement = myRangeInElement; + + if (rangeInElement == null) { + myRangeInElement = rangeInElement = calculateDefaultRangeInElement(); + } + return rangeInElement; + } + + @Override + public @NotNull String getCanonicalText() { + return getValue(); + } + + public @NotNull String getValue() { + String text = myElement.getText(); + TextRange range = getRangeInElement(); + + try { + return range.substring(text); + } catch (StringIndexOutOfBoundsException e) { + LOG.error("Wrong range in reference " + this + ": " + range + ". Reference text: '" + text + "'", e); + return text; + } + } + + protected TextRange calculateDefaultRangeInElement() { + return getManipulator(myElement).getRangeInElement(myElement); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index 62b72c975..6d9e17863 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -1,6 +1,9 @@ package io.nop.idea.plugin.lang.script.psi; +import java.util.ArrayList; +import java.util.List; import java.util.function.BiFunction; +import java.util.function.Supplier; import com.intellij.lang.ASTNode; import com.intellij.openapi.util.TextRange; @@ -12,8 +15,9 @@ import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiType; import com.intellij.psi.impl.PsiClassImplUtil; -import io.nop.idea.plugin.lang.script.reference.PsiFieldReference; -import io.nop.idea.plugin.lang.script.reference.PsiMethodReference; +import io.nop.idea.plugin.lang.script.reference.IdentifierReference; +import io.nop.idea.plugin.lang.script.reference.ObjectMethodReference; +import io.nop.idea.plugin.lang.script.reference.ObjectPropertyReference; import org.jetbrains.annotations.NotNull; import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_parameterizedTypeNode; @@ -283,47 +287,38 @@ public class ExpressionNode extends RuleSpecNode { // Note: // - 仅识别当前表达式的最后一个有效元素的引用,其余部分,由其子表达式做识别处理 // - 对象声明节点 ObjectDeclarationNode 的相关引用,由其自身负责构造 - // - 对构造函数 ParameterizedTypeNode 的相关引用,由其自身负责构造 + // - 对构造函数中的 QualifiedNameRootNode 的相关引用,由其自身负责构造 PsiElement firstChild = getFirstChild(); // 变量引用:abc - if (firstChild instanceof IdentifierNode i) { - TextRange textRange = i.getTextRangeInParent(); + if (firstChild instanceof IdentifierNode identifier) { + TextRange textRange = identifier.getTextRangeInParent(); + IdentifierReference ref = new IdentifierReference(this, identifier, textRange); - return i.createReferences(this, textRange); + return new PsiReference[] { ref }; } // 对象方法调用:a.b.c(1, 2) else if (isObjectMethodCall()) { - ExpressionNode obj = (ExpressionNode) firstChild; - PsiMethod method = getObjectMethod(); - - if (method != null) { - // Note: 需加上相对于当前表达式的对象偏移量 - TextRange methodTextRange = obj.getObjectMemberTextRange().shiftLeft(obj.getStartOffsetInParent()); - PsiMethodReference ref = new PsiMethodReference(this, method, methodTextRange); + ObjectMethodReference ref = new ObjectMethodReference(this); - return new PsiReference[] { ref }; - } + return new PsiReference[] { ref }; } // 对象属性访问:a.b.c - else if (isObjectMemberAccess()) { - PsiField prop = getObjectProperty(); - - if (prop != null) { - TextRange propTextRange = getObjectMemberTextRange(); - PsiFieldReference ref = new PsiFieldReference(this, prop, propTextRange); + else if (isObjectPropertyAccess()) { + ObjectPropertyReference ref = new ObjectPropertyReference(this); - return new PsiReference[] { ref }; - } + return new PsiReference[] { ref }; } // 函数调用:fn1(1, 2, 3) else if (isFunctionCall()) { ExpressionNode callee = (ExpressionNode) firstChild; - TextRange textRange = callee.getTextRangeInParent(); + TextRange textRange = callee.getTextRangeInParent(); IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); - return fn.createReferences(this, textRange); + IdentifierReference ref = new IdentifierReference(this, fn, textRange); + + return new PsiReference[] { ref }; } return PsiReference.EMPTY_ARRAY; @@ -337,13 +332,7 @@ public class ExpressionNode extends RuleSpecNode { public PsiClass getResultType() { PsiElement firstChild = getFirstChild(); - if (firstChild instanceof LiteralNode l) { - return l.getDataType(); - } // - else if (firstChild instanceof IdentifierNode i) { - return i.getDataType(); - } // - else if (isObjectConstructorCall()) { + if (isObjectConstructorCall()) { RuleSpecNode ptn = findChildByType(RULE_parameterizedTypeNode); QualifiedNameRootNode cons = ptn != null ? (QualifiedNameRootNode) ptn.getFirstChild() : null; @@ -354,9 +343,8 @@ public class ExpressionNode extends RuleSpecNode { PsiType returnType = method != null ? method.getReturnType() : null; return getPsiClassByPsiType(returnType); - } - // 若不是对象的方法调用,便是对象的属性访问 - else if (isObjectMemberAccess()) { + } // + else if (isObjectPropertyAccess()) { PsiField prop = getObjectProperty(); PsiType propType = prop != null ? prop.getType() : null; @@ -367,7 +355,7 @@ public class ExpressionNode extends RuleSpecNode { IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); // Note: 对应的是函数的返回值类型 - return fn.getDataType(); + return fn.getVarType(); } // else if (isArrowFunction()) { ArrowFunctionNode fn = (ArrowFunctionNode) firstChild; @@ -380,24 +368,54 @@ public class ExpressionNode extends RuleSpecNode { return array.getElementType(); } + List types = new ArrayList<>(); + PsiElement element = firstChild; + while (element != null) { + if (element instanceof LiteralNode l) { + types.add(l.getDataType()); + } // + else if (element instanceof IdentifierNode i) { + types.add(i.getVarType()); + } // + else if (element instanceof ExpressionNode e) { + types.add(e.getResultType()); + } + + element = element.getNextSibling(); + } + + if (types.size() == 1) { + return types.get(0); + } + // TODO 运算表达式,如 a + b return null; } - /** 当前表达式是否为对象成员(成员变量或方法)访问 */ - public boolean isObjectMemberAccess() { + /** + * 当前表达式是否为对象变量访问 + *

+ * 从最后一个对象成员的视角向上观察 + */ + public boolean isObjectPropertyAccess() { // a.b.c if (getFirstChild() instanceof ExpressionNode) { - return getLastChild() instanceof ObjectMemberNode; + return getLastChild() instanceof ObjectMemberNode // + && !(getParent().getLastChild() instanceof CalleeArgumentsNode); } return false; } - /** 当前表达式是否为对象方法调用 */ + /** + * 当前表达式是否为对象方法调用 + *

+ * 从最后一个对象成员的视角向上观察 + */ public boolean isObjectMethodCall() { // a.b.c() - if (getFirstChild() instanceof ExpressionNode obj) { - return obj.isObjectMemberAccess() && getLastChild() instanceof CalleeArgumentsNode; + if (getFirstChild() instanceof ExpressionNode) { + return getLastChild() instanceof ObjectMemberNode // + && getParent().getLastChild() instanceof CalleeArgumentsNode; } return false; } @@ -431,17 +449,20 @@ public class ExpressionNode extends RuleSpecNode { } /** 获取对象的方法 */ - protected PsiMethod getObjectMethod() { - ExpressionNode obj = (ExpressionNode) getFirstChild(); + public PsiMethod getObjectMethod() { + PsiMethod[] methods = getObjectMethods(); - PsiMethod[] methods = obj.getObjectMethods(); - PsiClass[] argTypes = getObjectMethodArgumentTypes(); + return filterMethodByArgs(methods, () -> ((ExpressionNode) getParent()).getObjectMethodArgumentTypes()); + } - return filterMethodByArgs(methods, argTypes); + /** 获取对象的属性 */ + public PsiField getObjectProperty() { + return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findFieldByName(objClass, memberName, true), + null); } /** 获取对象成员在当前表达式中的 {@link TextRange} */ - protected TextRange getObjectMemberTextRange() { + public TextRange getObjectMemberTextRange() { ObjectMemberNode member = (ObjectMemberNode) getLastChild(); return member.getTextRangeInParent(); @@ -453,12 +474,6 @@ public class ExpressionNode extends RuleSpecNode { PsiMethod.EMPTY_ARRAY); } - /** 获取对象的属性 */ - protected PsiField getObjectProperty() { - return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findFieldByName(objClass, memberName, true), - null); - } - /** 获取调用参数的类型列表 */ protected PsiClass[] getObjectMethodArgumentTypes() { CalleeArgumentsNode calleeArgs = (CalleeArgumentsNode) getLastChild(); @@ -480,12 +495,13 @@ public class ExpressionNode extends RuleSpecNode { return consumer.apply(objClass, memberName); } - protected PsiMethod filterMethodByArgs(PsiMethod[] methods, PsiClass[] args) { + protected PsiMethod filterMethodByArgs(PsiMethod[] methods, Supplier argsGetter) { // 只有唯一的方法,则直接返回 if (methods.length == 1) { return methods[0]; } + PsiClass[] args = argsGetter.get(); // 优先查找参数列表完全匹配的方法 for (PsiMethod method : methods) { PsiParameter[] params = method.getParameterList().getParameters(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java index 0412dfd0c..284b7cc14 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java @@ -33,17 +33,13 @@ import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_namedTyp * @date 2025-06-30 */ public class FunctionParameterDeclarationNode extends RuleSpecNode { - private IdentifierNode parameterName; public FunctionParameterDeclarationNode(@NotNull ASTNode node) { super(node); } public IdentifierNode getParameterName() { - if (parameterName == null || !parameterName.isValid()) { - parameterName = (IdentifierNode) getFirstChild().getFirstChild(); - } - return parameterName; + return (IdentifierNode) getFirstChild().getFirstChild(); } public PsiClass getParameterType() { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java index a57b4e140..8c2f32bca 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java @@ -1,14 +1,9 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; -import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReference; import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.lang.XLangVarDecl; -import io.nop.idea.plugin.lang.script.reference.PsiClassReference; -import io.nop.idea.plugin.lang.script.reference.VariableDeclarationReference; import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; @@ -30,37 +25,14 @@ public class IdentifierNode extends RuleSpecNode { super(node); } - public PsiReference @NotNull [] createReferences(PsiElement source, TextRange textRange) { - XLangVarDecl varDecl = getVarDecl(); - if (varDecl == null) { - return PsiReference.EMPTY_ARRAY; - } - - PsiElement element = varDecl.element(); - - if (element instanceof IdentifierNode id) { - VariableDeclarationReference ref = new VariableDeclarationReference(source, id, textRange); - - return new PsiReference[] { ref }; - } else if (element instanceof PsiClass clazz) { - PsiClassReference ref = new PsiClassReference(source, clazz, textRange); - - return new PsiReference[] { ref }; - } - return PsiReference.EMPTY_ARRAY; - } - - /** - * 获取变量的数据类型 - *

- * 若标识符为函数名,则返回函数的返回值类型 - */ - public PsiClass getDataType() { + /** 获取变量的类型 */ + public PsiClass getVarType() { XLangVarDecl varDecl = getVarDecl(); return varDecl != null ? varDecl.type() : null; } + /** 获取变量的定义信息 */ public XLangVarDecl getVarDecl() { String varName = getText(); XLangVarDecl decl = findVisibleVar(varName); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java index 03b84eb7b..b738d35cd 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -31,17 +31,13 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-29 */ public class ImportSourceNode extends RuleSpecNode { - private QualifiedNameNode qualifiedName; public ImportSourceNode(@NotNull ASTNode node) { super(node); } public QualifiedNameNode getQualifiedName() { - if (qualifiedName == null || !qualifiedName.isValid()) { - qualifiedName = (QualifiedNameNode) getFirstChild(); - } - return qualifiedName; + return (QualifiedNameNode) getFirstChild(); } /** 构造 Java 相关的引用对象,从而支持自动补全、引用跳转、文档显示等 */ diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java index fc057e736..924ea9ea9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java @@ -1,13 +1,13 @@ package io.nop.idea.plugin.lang.script.psi; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import com.intellij.lang.ASTNode; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; +import io.nop.idea.plugin.lang.script.reference.IdentifierReference; import org.jetbrains.annotations.NotNull; import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_objectProperties; @@ -74,8 +74,9 @@ public class ObjectDeclarationNode extends RuleSpecNode { TextRange textRange = propDecl.getTextRangeInParent(); IdentifierNode propNameNode = prop.getPropNameNode(); - PsiReference[] refs = propNameNode.createReferences(this, textRange); - Collections.addAll(result, refs); + IdentifierReference ref = new IdentifierReference(this, propNameNode, textRange); + + result.add(ref); } return result.toArray(PsiReference.EMPTY_ARRAY); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/PsiClassAndTextRange.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/PsiClassAndTextRange.java deleted file mode 100644 index f0ca03069..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/PsiClassAndTextRange.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.nop.idea.plugin.lang.script.psi; - -import java.util.Collection; - -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReference; -import io.nop.idea.plugin.lang.script.reference.PsiClassReference; -import org.jetbrains.annotations.NotNull; - -/** - * @author flytreeleft - * @date 2025-07-05 - */ -public record PsiClassAndTextRange(PsiClass clazz, TextRange textRange) { - - public static PsiReference @NotNull [] createReferences(PsiElement element, Collection list) { - return list.stream() - .filter(e -> e.clazz != null) - .map(e -> createReference(element, e)) - .toArray(PsiReference[]::new); - } - - public static @NotNull PsiReference createReference(PsiElement element, PsiClassAndTextRange cat) { - return new PsiClassReference(element, cat.clazz, cat.textRange); - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java index 2994e06c5..17f248ca6 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java @@ -28,17 +28,13 @@ import org.jetbrains.annotations.NotNull; * @date 2025-07-04 */ public class QualifiedNameNode extends RuleSpecNode { - private IdentifierNode identifier; public QualifiedNameNode(@NotNull ASTNode node) { super(node); } public IdentifierNode getIdentifier() { - if (identifier == null || !identifier.isValid()) { - identifier = (IdentifierNode) getFirstChild().getFirstChild(); - } - return identifier; + return (IdentifierNode) getFirstChild().getFirstChild(); } public String getLastName() { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java index 3e66293ff..2a4ace2ef 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java @@ -4,10 +4,11 @@ import java.util.ArrayList; import java.util.List; import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; -import io.nop.idea.plugin.utils.PsiClassHelper; +import io.nop.idea.plugin.lang.script.reference.QualifiedNameReference; import org.jetbrains.annotations.NotNull; /** @@ -40,80 +41,70 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-30 */ public class QualifiedNameRootNode extends RuleSpecNode { - private QualifiedNameNode qualifiedName; public QualifiedNameRootNode(@NotNull ASTNode node) { super(node); } public QualifiedNameNode getQualifiedName() { - if (qualifiedName == null || !qualifiedName.isValid()) { - qualifiedName = (QualifiedNameNode) getFirstChild(); - } - return qualifiedName; + return (QualifiedNameNode) getFirstChild(); } @Override protected PsiReference @NotNull [] doGetReferences() { - List result = getClassAndTextRanges(); - - // 若未找到已导入的类,则尝试按包查找 - if (result.isEmpty()) { - String fqn = getText(); - - return PsiClassHelper.createJavaClassReferences(this, fqn, 0); - } + List result = createReferences(); - return PsiClassAndTextRange.createReferences(this, result); + return result.toArray(PsiReference.EMPTY_ARRAY); } public PsiClass getQualifiedType() { - List result = getClassAndTextRanges(); + List result = createReferences(); + if (result.isEmpty()) { + return null; + } String fqn = getText().replace(" ", ""); - if (result.isEmpty()) { - // 按全类名处理 - return PsiClassHelper.findClass(getProject(), fqn); + QualifiedNameReference ref = result.get(result.size() - 1); + PsiElement element = ref.resolve(); + + if (!(element instanceof PsiClass clazz)) { + return null; } - PsiClass clazz = result.get(result.size() - 1).clazz(); - String clazzName = clazz.getQualifiedName(); + String className = clazz.getQualifiedName(); - return clazzName != null // - && (fqn.equals(clazzName) // - || clazzName.endsWith('.' + fqn)) // + return className != null // + && (fqn.equals(className) // + || className.endsWith('.' + fqn)) // ? clazz : null; } - protected List getClassAndTextRanges() { + protected List createReferences() { QualifiedNameNode qnn = getQualifiedName(); - IdentifierNode identifier = qnn.getIdentifier(); - PsiClass clazz = identifier.getDataType(); - - List result = new ArrayList<>(); - findInnerClass(clazz, qnn, 0, result); + List result = new ArrayList<>(); + createReferences(null, qnn, 0, result); return result; } - protected void findInnerClass( - PsiClass clazz, QualifiedNameNode qnn, int offset, List result + protected void createReferences( + QualifiedNameReference parentReference, QualifiedNameNode qnn, int offset, + List result ) { - if (clazz == null) { - return; - } + IdentifierNode identifier = qnn.getIdentifier(); + // Note: 取相对于 qnn 的 TextRange 并做偏移 + TextRange textRange = identifier.getParent().getTextRangeInParent().shiftRight(offset); - result.add(new PsiClassAndTextRange(clazz, qnn.getTextRangeInParent().shiftRight(offset))); + QualifiedNameReference ref = new QualifiedNameReference(this, identifier, textRange, parentReference); + + result.add(ref); PsiElement sub = qnn.getLastChild(); if (!(sub instanceof QualifiedNameNode subQnn)) { return; } - String subName = subQnn.getIdentifier().getText(); - PsiClass subClazz = clazz.findInnerClassByName(subName, true); - - findInnerClass(subClazz, subQnn, qnn.getStartOffsetInParent(), result); + createReferences(ref, subQnn, subQnn.getStartOffsetInParent() + offset, result); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java index 646f8bfda..2438cad6a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java @@ -1,12 +1,11 @@ package io.nop.idea.plugin.lang.script.psi; import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; -import io.nop.api.core.util.Symbol; -import io.nop.idea.plugin.lang.script.reference.PsiClassReference; -import io.nop.idea.plugin.utils.PsiClassHelper; +import io.nop.idea.plugin.lang.script.reference.PredefinedTypeReference; import org.jetbrains.annotations.NotNull; /** @@ -21,48 +20,28 @@ import org.jetbrains.annotations.NotNull; * @date 2025-07-05 */ public class TypeNameNodePredefinedNode extends RuleSpecNode { - private PsiElement typeName; public TypeNameNodePredefinedNode(@NotNull ASTNode node) { super(node); } public PsiElement getTypeName() { - if (typeName == null || !typeName.isValid()) { - typeName = getLastChild(); - } - return typeName; + return getLastChild(); } public PsiClass getPredefinedType() { - // 仅包含确定类型,详见 nop-xlang/model/antlr/XLangTypeSystem.g4 - PsiElement typeNameNode = getTypeName(); - String typeName = typeNameNode.getText(); + String typeName = getTypeName().getText(); - Class typeClass = switch (typeName) { - case "any" -> Object.class; - case "number" -> Number.class; - case "boolean" -> Boolean.class; - case "string" -> String.class; - case "symbol" -> Symbol.class; - case "void" -> Void.class; - default -> null; - }; - - return typeClass != null ? PsiClassHelper.findClass(getProject(), typeClass.getName()) : null; + return PredefinedTypeReference.getPredefinedType(getProject(), typeName); } @Override protected PsiReference @NotNull [] doGetReferences() { - PsiClass clazz = getPredefinedType(); - if (clazz == null) { - return PsiReference.EMPTY_ARRAY; - } + PsiElement typeName = getTypeName(); + TextRange textRange = typeName.getTextRangeInParent(); - PsiElement typeNameNode = getTypeName(); + PredefinedTypeReference ref = new PredefinedTypeReference(this, typeName, textRange); - return new PsiReference[] { - new PsiClassReference(this, clazz, typeNameNode.getTextRangeInParent()) - }; + return new PsiReference[] { ref }; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java index 924a764cd..1f08caef7 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java @@ -25,6 +25,47 @@ import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_expressi * PsiElement(DecimalIntegerLiteral)('123') * * + * map = new HashMap<String, List>(): + *

+ * VariableDeclaratorNode(variableDeclarator)
+ *   RuleSpecNode(ast_identifierOrPattern)
+ *     IdentifierNode(identifier)
+ *       PsiElement(Identifier)('map')
+ *   PsiWhiteSpace(' ')
+ *   RuleSpecNode(expression_initializer)
+ *     PsiElement('=')('=')
+ *     PsiWhiteSpace(' ')
+ *     ExpressionNode(expression_single)
+ *       PsiElement('new')('new')
+ *       PsiWhiteSpace(' ')
+ *       RuleSpecNode(parameterizedTypeNode)
+ *         QualifiedNameRootNode(qualifiedName_)
+ *           QualifiedNameNode(qualifiedName)
+ *             RuleSpecNode(qualifiedName_name_)
+ *               IdentifierNode(identifier)
+ *                 PsiElement(Identifier)('HashMap')
+ *         RuleSpecNode(typeArguments_)
+ *           PsiElement('<')('<')
+ *           RuleSpecNode(namedTypeNode)
+ *             QualifiedNameRootNode(qualifiedName_)
+ *               QualifiedNameNode(qualifiedName)
+ *                 RuleSpecNode(qualifiedName_name_)
+ *                   IdentifierNode(identifier)
+ *                     PsiElement(Identifier)('String')
+ *           PsiElement(',')(',')
+ *           PsiWhiteSpace(' ')
+ *           RuleSpecNode(namedTypeNode)
+ *             QualifiedNameRootNode(qualifiedName_)
+ *               QualifiedNameNode(qualifiedName)
+ *                 RuleSpecNode(qualifiedName_name_)
+ *                   IdentifierNode(identifier)
+ *                     PsiElement(Identifier)('List')
+ *           PsiElement('>')('>')
+ *       CalleeArgumentsNode(arguments_)
+ *         PsiElement('(')('(')
+ *         PsiElement(')')(')')
+ * 
+ * * @author flytreeleft * @date 2025-07-03 */ diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/VariableDeclarationReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java similarity index 32% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/VariableDeclarationReference.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java index 10d0606a2..841dc90d7 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/VariableDeclarationReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java @@ -2,32 +2,47 @@ package io.nop.idea.plugin.lang.script.reference; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReferenceBase; +import com.intellij.util.IncorrectOperationException; +import io.nop.idea.plugin.lang.XLangVarDecl; +import io.nop.idea.plugin.lang.reference.XLangReferenceBase; import io.nop.idea.plugin.lang.script.psi.IdentifierNode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** + * {@link IdentifierNode} 的引用 + *

+ * 涉及对变量和类名的引用 + * * @author flytreeleft - * @date 2025-07-04 + * @date 2025-07-06 */ -public class VariableDeclarationReference extends PsiReferenceBase { +public class IdentifierReference extends XLangReferenceBase { private final IdentifierNode identifier; - public VariableDeclarationReference( - @NotNull PsiElement element, IdentifierNode identifier, TextRange rangeInElement - ) { - super(element, rangeInElement); + public IdentifierReference(PsiElement myElement, IdentifierNode identifier, TextRange myRangeInElement) { + super(myElement, myRangeInElement); this.identifier = identifier; } @Override - public @Nullable PsiElement resolve() { - return identifier; + public @Nullable PsiElement resolveInner() { + if (!identifier.isValid()) { + return null; + } + + XLangVarDecl varDecl = identifier.getVarDecl(); + + return varDecl != null ? varDecl.element() : null; + } + + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + return null; } @Override - public Object @NotNull [] getVariants() { - return super.getVariants(); + public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { + return null; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java new file mode 100644 index 000000000..0891ba8f4 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java @@ -0,0 +1,42 @@ +package io.nop.idea.plugin.lang.script.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.util.IncorrectOperationException; +import io.nop.idea.plugin.lang.reference.XLangReferenceBase; +import io.nop.idea.plugin.lang.script.psi.ExpressionNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对象方法引用 + * + * @author flytreeleft + * @date 2025-07-06 + */ +public class ObjectMethodReference extends XLangReferenceBase { + + public ObjectMethodReference(ExpressionNode myElement) { + super(myElement, null); + } + + @Override + protected TextRange calculateDefaultRangeInElement() { + return ((ExpressionNode) myElement).getObjectMemberTextRange(); + } + + @Override + public @Nullable PsiElement resolveInner() { + return ((ExpressionNode) myElement).getObjectMethod(); + } + + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + return null; + } + + @Override + public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { + return null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectPropertyReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectPropertyReference.java new file mode 100644 index 000000000..dcea42ec6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectPropertyReference.java @@ -0,0 +1,44 @@ +package io.nop.idea.plugin.lang.script.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.util.IncorrectOperationException; +import io.nop.idea.plugin.lang.reference.XLangReferenceBase; +import io.nop.idea.plugin.lang.script.psi.ExpressionNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对象属性引用 + *

+ * 根据 {@link ExpressionNode} 表达式得到唯一的对象属性 + * + * @author flytreeleft + * @date 2025-07-06 + */ +public class ObjectPropertyReference extends XLangReferenceBase { + + public ObjectPropertyReference(ExpressionNode myElement) { + super(myElement, null); + } + + @Override + protected TextRange calculateDefaultRangeInElement() { + return ((ExpressionNode) myElement).getObjectMemberTextRange(); + } + + @Override + public @Nullable PsiElement resolveInner() { + return ((ExpressionNode) myElement).getObjectProperty(); + } + + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + return null; + } + + @Override + public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { + return null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java new file mode 100644 index 000000000..f50e6ff05 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java @@ -0,0 +1,63 @@ +package io.nop.idea.plugin.lang.script.reference; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.util.IncorrectOperationException; +import io.nop.api.core.util.Symbol; +import io.nop.idea.plugin.lang.reference.XLangReferenceBase; +import io.nop.idea.plugin.utils.PsiClassHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对 stringnumber 等内置类型的引用 + * + * @author flytreeleft + * @date 2025-07-06 + */ +public class PredefinedTypeReference extends XLangReferenceBase { + private final PsiElement typeName; + + public PredefinedTypeReference(PsiElement myElement, PsiElement typeName, TextRange myRangeInElement) { + super(myElement, myRangeInElement); + this.typeName = typeName; + } + + @Override + public @Nullable PsiElement resolveInner() { + if (!typeName.isValid()) { + return null; + } + + Project project = myElement.getProject(); + + return getPredefinedType(project, typeName.getText()); + } + + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + return null; + } + + @Override + public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { + return null; + } + + public static PsiClass getPredefinedType(Project project, String typeName) { + // 仅包含确定类型,详见 nop-xlang/model/antlr/XLangTypeSystem.g4 + Class typeClass = switch (typeName) { + case "any" -> Object.class; + case "number" -> Number.class; + case "boolean" -> Boolean.class; + case "string" -> String.class; + case "symbol" -> Symbol.class; + case "void" -> Void.class; + default -> null; + }; + + return typeClass != null ? PsiClassHelper.findClass(project, typeClass.getName()) : null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiClassReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiClassReference.java deleted file mode 100644 index db67c73ce..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiClassReference.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.nop.idea.plugin.lang.script.reference; - -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReferenceBase; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * @author flytreeleft - * @date 2025-07-04 - */ -public class PsiClassReference extends PsiReferenceBase { - private final PsiClass clazz; - - public PsiClassReference( - @NotNull PsiElement element, PsiClass clazz, TextRange rangeInElement - ) { - super(element, rangeInElement); - this.clazz = clazz; - } - - @Override - public @Nullable PsiElement resolve() { - return clazz; - } - - @Override - public Object @NotNull [] getVariants() { - return super.getVariants(); - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiFieldReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiFieldReference.java deleted file mode 100644 index 1456be8af..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiFieldReference.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.nop.idea.plugin.lang.script.reference; - -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiReferenceBase; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * 对类属性的引用 - * - * @author flytreeleft - * @date 2025-07-01 - */ -public class PsiFieldReference extends PsiReferenceBase { - private final PsiField prop; - - public PsiFieldReference(@NotNull PsiElement element, PsiField prop, TextRange rangeInElement) { - super(element, rangeInElement); - this.prop = prop; - } - - @Override - public @Nullable PsiElement resolve() { - return prop; - } - - @Override - public Object @NotNull [] getVariants() { - return super.getVariants(); - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiMethodReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiMethodReference.java deleted file mode 100644 index 646f45953..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PsiMethodReference.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.nop.idea.plugin.lang.script.reference; - -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiMethod; -import com.intellij.psi.PsiReferenceBase; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * 对类方法的引用 - * - * @author flytreeleft - * @date 2025-07-01 - */ -public class PsiMethodReference extends PsiReferenceBase { - private final PsiMethod method; - - public PsiMethodReference(@NotNull PsiElement element, PsiMethod method, TextRange rangeInElement) { - super(element, rangeInElement); - this.method = method; - } - - @Override - public @Nullable PsiElement resolve() { - return method; - } - - @Override - public Object @NotNull [] getVariants() { - return super.getVariants(); - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java new file mode 100644 index 000000000..e0a647dc6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java @@ -0,0 +1,85 @@ +package io.nop.idea.plugin.lang.script.reference; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiPackage; +import com.intellij.util.IncorrectOperationException; +import io.nop.idea.plugin.lang.reference.XLangReferenceBase; +import io.nop.idea.plugin.lang.script.psi.IdentifierNode; +import io.nop.idea.plugin.utils.PsiClassHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author flytreeleft + * @date 2025-07-06 + */ +public class QualifiedNameReference extends XLangReferenceBase { + private final IdentifierNode identifier; + private final QualifiedNameReference parentReference; + + public QualifiedNameReference( + PsiElement myElement, IdentifierNode identifier, TextRange myRangeInElement, + QualifiedNameReference parentReference + ) { + super(myElement, myRangeInElement); + this.identifier = identifier; + this.parentReference = parentReference; + } + + @Override + public @Nullable PsiElement resolveInner() { + if (!identifier.isValid()) { + return null; + } + + Project project = myElement.getProject(); + String subName = identifier.getText(); + + // 最顶层标志符 + if (parentReference == null) { + // 先尝试查找引用的变量类型 + PsiClass clazz = identifier.getVarType(); + if (clazz != null) { + return clazz; + } + + // ,再按包名查找 + return PsiClassHelper.findPackage(project, subName); + } else { + PsiElement parentElement = parentReference.resolve(); + if (parentElement == null) { + return null; + } + + if (parentElement instanceof PsiClass clazz) { + return clazz.findInnerClassByName(subName, true); + } // + else if (parentElement instanceof PsiPackage pkg) { + subName = pkg.getQualifiedName() + '.' + subName; + + PsiPackage subPkg = PsiClassHelper.findPackage(project, subName); + if (subPkg != null) { + return subPkg; + } + + // 若包不存在,则可能是类 + return PsiClassHelper.findClass(project, subName); + } + } + + return null; + } + + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + return null; + } + + @Override + public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { + return null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java index 538cc2f74..d69937d1b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java @@ -6,7 +6,6 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.xml.XmlElement; -import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.util.ArrayUtil; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -22,14 +21,6 @@ import org.jetbrains.annotations.Nullable; public class XLangVfsFileReference extends PsiReferenceBase implements XLangReference { private final PsiFile file; - /** - * @param refElement - * 与引用直接相关的元素。例如绑定属性值与文件的引用关系,则该参数需为不包含引号的、类型为 - * {@link XmlElementType#XML_ATTRIBUTE_VALUE_TOKEN} 类型的元素。 - * 如果在该元素内以分隔符分隔了多个引用,则需要通过参数 textRange - * 指定对应引用的关联文本所在的文本范围,该范围相对于该元素,从 0 开始计算。 - * 相关处理逻辑见 {@link com.intellij.psi.impl.SharedPsiElementImplUtil#addReferences SharedPsiElementImplUtil#addReferences}) - */ public XLangVfsFileReference( @NotNull XmlElement refElement, TextRange textRange, // @NotNull PsiFile file diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index 9bc5381cc..2fef14d0c 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -34,6 +34,8 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { let abc = new String("abc"); let abc = new Abc.Def("abc"); let abc = new java.lang.String("abc"); + const map = new HashMap(); + // let def = new String("def").trim(); let def = 123, lmn = 456 + abc; const c = a.b.c; diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java index e69fd1a15..fffae020b 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -108,12 +108,25 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { const handler = new XJsonDomainHandler(); """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); assertReference(""" - const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const sub = new XJsonDomainHandler.Sub.Sub(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub"); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const sub = new XJsonDomainHandler.Sub.Sub(); """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub"); + assertReference(""" + const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler(); + """, "io.nop.xlang.xdef"); + assertReference(""" + const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + assertReference(""" + const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + assertReference(""" + const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub"); assertReference(""" const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub"); @@ -246,6 +259,24 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { // """, "java.lang.Integer#byteValue"); } + public void testGenericTypeReference() { + assertReference(""" + import java.util.HashMap; + import java.util.List; + const s = new HashMap(); + """, "java.util.HashMap"); + assertReference(""" + import java.util.HashMap; + import java.util.List; + const s = new HashMapring, List>(); + """, "java.lang.String"); + assertReference(""" + import java.util.HashMap; + import java.util.List; + const s = new HashMapst>(); + """, "java.util.List"); + } + /** 通过在 text 中插入 <caret> 代表光标位置 */ private void assertReference(String text, String expected) { myFixture.configureByText("sample." + ext, text); diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast index 55f352571..41c2a89ba 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast @@ -305,6 +305,55 @@ FILE RuleSpecNode(eos__) PsiElement(';')(';') PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + PsiWhiteSpace(' ') + VariableDeclaratorsNode(variableDeclarators_) + VariableDeclaratorNode(variableDeclarator) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('map') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + PsiElement('new')('new') + PsiWhiteSpace(' ') + RuleSpecNode(parameterizedTypeNode) + QualifiedNameRootNode(qualifiedName_) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('HashMap') + RuleSpecNode(typeArguments_) + PsiElement('<')('<') + RuleSpecNode(namedTypeNode) + QualifiedNameRootNode(qualifiedName_) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('String') + PsiElement(',')(',') + PsiWhiteSpace(' ') + RuleSpecNode(namedTypeNode) + QualifiedNameRootNode(qualifiedName_) + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('List') + PsiElement('>')('>') + CalleeArgumentsNode(arguments_) + PsiElement('(')('(') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + PsiElement(SingleLineComment)('//') + PsiWhiteSpace('\n') TopLevelStatementNode(ast_topLevelStatement) StatementNode(statement) VariableDeclarationNode(variableDeclaration) -- Gitee From a75a54b92e77cf0ac6cc4cfa4c3541fe47b0da6a Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 6 Jul 2025 15:45:46 +0800 Subject: [PATCH 38/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AF=B9=20XLang=20Script=20=E4=B8=AD=E6=9C=AA=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=8B=AC=E5=8F=B7=E7=9A=84=E5=AF=B9=E8=B1=A1=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/script/psi/ExpressionNode.java | 41 +++++++++++++------ ...erence.java => ObjectMemberReference.java} | 10 ++--- .../lang/TestXLangScriptReferences.java | 10 +++++ .../test/resources/_vfs/test/reference/a.xlib | 2 + 4 files changed, 44 insertions(+), 19 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/{ObjectPropertyReference.java => ObjectMemberReference.java} (78%) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index 6d9e17863..a421430c6 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -16,8 +16,8 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.PsiType; import com.intellij.psi.impl.PsiClassImplUtil; import io.nop.idea.plugin.lang.script.reference.IdentifierReference; +import io.nop.idea.plugin.lang.script.reference.ObjectMemberReference; import io.nop.idea.plugin.lang.script.reference.ObjectMethodReference; -import io.nop.idea.plugin.lang.script.reference.ObjectPropertyReference; import org.jetbrains.annotations.NotNull; import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_parameterizedTypeNode; @@ -304,8 +304,8 @@ public class ExpressionNode extends RuleSpecNode { return new PsiReference[] { ref }; } // 对象属性访问:a.b.c - else if (isObjectPropertyAccess()) { - ObjectPropertyReference ref = new ObjectPropertyReference(this); + else if (isObjectMemberAccess()) { + ObjectMemberReference ref = new ObjectMemberReference(this); return new PsiReference[] { ref }; } @@ -344,9 +344,13 @@ public class ExpressionNode extends RuleSpecNode { return getPsiClassByPsiType(returnType); } // - else if (isObjectPropertyAccess()) { - PsiField prop = getObjectProperty(); - PsiType propType = prop != null ? prop.getType() : null; + else if (isObjectMemberAccess()) { + PsiElement member = getObjectMember(); + if (!(member instanceof PsiField prop)) { + return null; + } + + PsiType propType = prop.getType(); return getPsiClassByPsiType(propType); } // @@ -393,11 +397,14 @@ public class ExpressionNode extends RuleSpecNode { } /** - * 当前表达式是否为对象变量访问 + * 当前表达式是否为对象成员(属性或方法)访问 *

* 从最后一个对象成员的视角向上观察 + *

+ * 在父节点未包含 {@link CalleeArgumentsNode} 节点时, + * 当前对象成员可能是变量,也可能是方法 */ - public boolean isObjectPropertyAccess() { + public boolean isObjectMemberAccess() { // a.b.c if (getFirstChild() instanceof ExpressionNode) { return getLastChild() instanceof ObjectMemberNode // @@ -455,10 +462,17 @@ public class ExpressionNode extends RuleSpecNode { return filterMethodByArgs(methods, () -> ((ExpressionNode) getParent()).getObjectMethodArgumentTypes()); } - /** 获取对象的属性 */ - public PsiField getObjectProperty() { - return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findFieldByName(objClass, memberName, true), - null); + /** 获取对象的成员(属性或方法) */ + public PsiElement getObjectMember() { + return getObjectMember((objClass, memberName) -> { + PsiField prop = PsiClassImplUtil.findFieldByName(objClass, memberName, true); + if (prop != null) { + return prop; + } + + PsiMethod[] methods = PsiClassImplUtil.findMethodsByName(objClass, memberName, true); + return methods.length > 0 ? methods[0] : null; + }, null); } /** 获取对象成员在当前表达式中的 {@link TextRange} */ @@ -518,7 +532,8 @@ public class ExpressionNode extends RuleSpecNode { } } - return null; + // 若都不匹配,则取第一个 + return methods.length > 0 ? methods[0] : null; } protected boolean matchMethodParams(PsiParameter[] params, PsiClass[] args) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectPropertyReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java similarity index 78% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectPropertyReference.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java index dcea42ec6..dd47cb5e5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectPropertyReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java @@ -9,16 +9,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** - * 对象属性引用 - *

- * 根据 {@link ExpressionNode} 表达式得到唯一的对象属性 + * 对象成员(属性或方法)引用 * * @author flytreeleft * @date 2025-07-06 */ -public class ObjectPropertyReference extends XLangReferenceBase { +public class ObjectMemberReference extends XLangReferenceBase { - public ObjectPropertyReference(ExpressionNode myElement) { + public ObjectMemberReference(ExpressionNode myElement) { super(myElement, null); } @@ -29,7 +27,7 @@ public class ObjectPropertyReference extends XLangReferenceBase { @Override public @Nullable PsiElement resolveInner() { - return ((ExpressionNode) myElement).getObjectProperty(); + return ((ExpressionNode) myElement).getObjectMember(); } @Override diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java index fffae020b..d26118ded 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -55,6 +55,16 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { let a = b; """, "java.lang.String#trim"); + assertReference(""" + Integer.valueOf; + """, "java.lang.Integer#valueOf"); + assertReference(""" + Integer.valueOf(); + """, "java.lang.Integer#valueOf"); + assertReference(""" + Integer.valueOf('123'); + """, "java.lang.Integer#valueOf"); + assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler(); diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index 827735656..b998d9262 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -21,6 +21,8 @@ const s = "abc".startsWith("a"); const fn = (a: any, b: string) => a + b; fn(1, 2); + Integer.valueOf; + Integer.valueOf('a'); while (true) { let a = 's' instanceof string; -- Gitee From ec04e964a67d27253e85af7b45600caed3589e8a Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 6 Jul 2025 22:53:13 +0800 Subject: [PATCH 39/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AF=B9=20XLang=20Script=20=E4=B8=AD=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E7=B1=BB=E5=90=8D=E3=80=81=E5=8C=85=E5=90=8D=E3=80=81?= =?UTF-8?q?=E7=B1=BB=E5=B1=9E=E6=80=A7=E5=92=8C=E6=96=B9=E6=B3=95=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E9=87=8D=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/reference/XLangReferenceBase.java | 15 ++ .../script/XLangScriptElementManipulator.java | 68 +++++++++ .../script/psi/QualifiedNameRootNode.java | 5 +- .../script/reference/IdentifierReference.java | 12 -- .../reference/ObjectMemberReference.java | 15 +- .../reference/ObjectMethodReference.java | 23 +-- .../reference/PredefinedTypeReference.java | 12 -- .../reference/QualifiedNameReference.java | 23 ++- .../src/main/resources/META-INF/plugin.xml | 3 + .../plugin/lang/TestXLangScriptRename.java | 136 ++++++++++++++++++ 10 files changed, 256 insertions(+), 56 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptElementManipulator.java create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java index 922c695c0..318bbec9b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java @@ -4,6 +4,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.impl.source.resolve.reference.impl.CachingReference; +import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; /** @@ -67,6 +68,20 @@ public abstract class XLangReferenceBase extends CachingReference { } } + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + return null; + } + + @Override + public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { + return null; + } + + public void setRangeInElement(TextRange rangeInElement) { + myRangeInElement = rangeInElement; + } + protected TextRange calculateDefaultRangeInElement() { return getManipulator(myElement).getRangeInElement(myElement); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptElementManipulator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptElementManipulator.java new file mode 100644 index 000000000..3b725ac5e --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptElementManipulator.java @@ -0,0 +1,68 @@ +package io.nop.idea.plugin.lang.script; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.AbstractElementManipulator; +import com.intellij.psi.ElementManipulators; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFileFactory; +import com.intellij.util.IncorrectOperationException; +import io.nop.idea.plugin.lang.script.psi.Identifier; +import io.nop.idea.plugin.lang.script.psi.ImportDeclarationNode; +import io.nop.idea.plugin.lang.script.psi.ImportSourceNode; +import io.nop.idea.plugin.lang.script.psi.QualifiedNameNode; +import io.nop.idea.plugin.lang.script.psi.RuleSpecNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 负责处理对 XLang Script 中 AST 节点元素的修改 + *

+ * 通过 {@link ElementManipulators#getManipulator} 获取其在 + * plugin.xml 中注册的实例 + * + * @author flytreeleft + * @date 2025-07-06 + */ +public class XLangScriptElementManipulator extends AbstractElementManipulator { + + @Override + public @Nullable RuleSpecNode handleContentChange( + @NotNull RuleSpecNode element, @NotNull TextRange rangeInElement, String newContent + ) throws IncorrectOperationException { + TextRange elementTextRange = element.getTextRangeInParent().shiftLeft(element.getStartOffsetInParent()); + boolean needToReplaceElement = elementTextRange.equals(rangeInElement); + + // 对导入的类名做整体替换,但在 needToReplaceWhole = false 时,对包名做局部替换 + if (element instanceof ImportSourceNode imp && needToReplaceElement) { + PsiElement node = PsiFileFactory.getInstance(element.getProject()) + .createFileFromText(element.getLanguage(), "import " + newContent); + while (node != null) { + if (node instanceof QualifiedNameNode) { + imp.getQualifiedName().replace(node); + break; + } + + if (node instanceof ImportDeclarationNode) { + // 跳过 import 和 空白 + node = node.getFirstChild().getNextSibling().getNextSibling(); + } else { + node = node.getFirstChild(); + } + } + + return element; + } + + // Note: 取相对于 element 的偏移量 + int offset = rangeInElement.getStartOffset(); + // 得到待修改的元素 + PsiElement target = element.findElementAt(offset); + + if (target instanceof Identifier id) { + // Note: 其会直接更新 element 的树结构 + id.replaceWithText(newContent); + } + + return element; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java index 2a4ace2ef..d9d76d63f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java @@ -16,7 +16,7 @@ import org.jetbrains.annotations.NotNull; *

* String: *

- * RuleSpecNode(qualifiedName_)
+ * QualifiedNameRootNode(qualifiedName_)
  *   QualifiedNameNode(qualifiedName)
  *     RuleSpecNode(qualifiedName_name_)
  *       IdentifierNode(identifier)
@@ -25,7 +25,7 @@ import org.jetbrains.annotations.NotNull;
  *
  * Abc.Def:
  * 
- * RuleSpecNode(qualifiedName_)
+ * QualifiedNameRootNode(qualifiedName_)
  *   QualifiedNameNode(qualifiedName)
  *     RuleSpecNode(qualifiedName_name_)
  *       IdentifierNode(identifier)
@@ -97,7 +97,6 @@ public class QualifiedNameRootNode extends RuleSpecNode {
         TextRange textRange = identifier.getParent().getTextRangeInParent().shiftRight(offset);
 
         QualifiedNameReference ref = new QualifiedNameReference(this, identifier, textRange, parentReference);
-
         result.add(ref);
 
         PsiElement sub = qnn.getLastChild();
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java
index 841dc90d7..e1fe21d6c 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java
@@ -2,11 +2,9 @@ package io.nop.idea.plugin.lang.script.reference;
 
 import com.intellij.openapi.util.TextRange;
 import com.intellij.psi.PsiElement;
-import com.intellij.util.IncorrectOperationException;
 import io.nop.idea.plugin.lang.XLangVarDecl;
 import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
 import io.nop.idea.plugin.lang.script.psi.IdentifierNode;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -35,14 +33,4 @@ public class IdentifierReference extends XLangReferenceBase {
 
         return varDecl != null ? varDecl.element() : null;
     }
-
-    @Override
-    public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
-        return null;
-    }
-
-    @Override
-    public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
-        return null;
-    }
 }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java
index dd47cb5e5..0ea7e2357 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java
@@ -1,6 +1,7 @@
 package io.nop.idea.plugin.lang.script.reference;
 
 import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.ElementManipulator;
 import com.intellij.psi.PsiElement;
 import com.intellij.util.IncorrectOperationException;
 import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
@@ -32,11 +33,15 @@ public class ObjectMemberReference extends XLangReferenceBase {
 
     @Override
     public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
-        return null;
-    }
+        PsiElement element = myElement;
+        TextRange rangeInElement = getRangeInElement();
 
-    @Override
-    public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
-        return null;
+        ElementManipulator manipulator = getManipulator(element);
+        element = manipulator.handleContentChange(element, rangeInElement, newElementName);
+
+        rangeInElement = ((ExpressionNode) element).getObjectMemberTextRange();
+        setRangeInElement(rangeInElement);
+
+        return element;
     }
 }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java
index 0891ba8f4..013f93efc 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java
@@ -1,11 +1,7 @@
 package io.nop.idea.plugin.lang.script.reference;
 
-import com.intellij.openapi.util.TextRange;
 import com.intellij.psi.PsiElement;
-import com.intellij.util.IncorrectOperationException;
-import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
 import io.nop.idea.plugin.lang.script.psi.ExpressionNode;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -14,29 +10,14 @@ import org.jetbrains.annotations.Nullable;
  * @author flytreeleft
  * @date 2025-07-06
  */
-public class ObjectMethodReference extends XLangReferenceBase {
+public class ObjectMethodReference extends ObjectMemberReference {
 
     public ObjectMethodReference(ExpressionNode myElement) {
-        super(myElement, null);
-    }
-
-    @Override
-    protected TextRange calculateDefaultRangeInElement() {
-        return ((ExpressionNode) myElement).getObjectMemberTextRange();
+        super(myElement);
     }
 
     @Override
     public @Nullable PsiElement resolveInner() {
         return ((ExpressionNode) myElement).getObjectMethod();
     }
-
-    @Override
-    public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
-        return null;
-    }
-
-    @Override
-    public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
-        return null;
-    }
 }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java
index f50e6ff05..1946959a6 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java
@@ -4,11 +4,9 @@ import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.TextRange;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElement;
-import com.intellij.util.IncorrectOperationException;
 import io.nop.api.core.util.Symbol;
 import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
 import io.nop.idea.plugin.utils.PsiClassHelper;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -36,16 +34,6 @@ public class PredefinedTypeReference extends XLangReferenceBase {
         return getPredefinedType(project, typeName.getText());
     }
 
-    @Override
-    public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
-        return null;
-    }
-
-    @Override
-    public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
-        return null;
-    }
-
     public static PsiClass getPredefinedType(Project project, String typeName) {
         // 仅包含确定类型,详见 nop-xlang/model/antlr/XLangTypeSystem.g4
         Class typeClass = switch (typeName) {
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
index e0a647dc6..5ed325ad0 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
@@ -7,6 +7,7 @@ import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiPackage;
 import com.intellij.util.IncorrectOperationException;
 import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
+import io.nop.idea.plugin.lang.script.psi.Identifier;
 import io.nop.idea.plugin.lang.script.psi.IdentifierNode;
 import io.nop.idea.plugin.utils.PsiClassHelper;
 import org.jetbrains.annotations.NotNull;
@@ -73,13 +74,29 @@ public class QualifiedNameReference extends XLangReferenceBase {
         return null;
     }
 
+    /** 在构造函数中,用于修改包名 */
     @Override
     public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
-        return null;
+        PsiElement element = myElement;
+        TextRange rangeInElement = getRangeInElement();
+
+        // 直接替换名字
+        ((Identifier) identifier.getFirstChild()).replaceWithText(newElementName);
+
+        rangeInElement = identifier.getTextRangeInParent().shiftRight(rangeInElement.getStartOffset());
+        setRangeInElement(rangeInElement);
+
+        return element;
     }
 
+    /** 在构造函数中,用于修改类名 */
     @Override
-    public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
-        return null;
+    public PsiElement bindToElement(@NotNull PsiElement source) throws IncorrectOperationException {
+        String newName = null;
+        if (source instanceof PsiClass clazz) {
+            newName = clazz.getName();
+        }
+
+        return newName != null ? handleElementRename(newName) : null;
     }
 }
diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml
index 2eca83215..1ec768cdf 100644
--- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml
+++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml
@@ -117,6 +117,9 @@
                                 implementationClass="io.nop.idea.plugin.lang.script.XLangScriptSyntaxHighlighter"/>
         
+        
+        
         
diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java
new file mode 100644
index 000000000..fe5ef96c2
--- /dev/null
+++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2017-2024 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog:   https://www.zhihu.com/people/canonical-entropy
+ * Gitee:  https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+package io.nop.idea.plugin.lang;
+
+import com.intellij.psi.PsiFile;
+import io.nop.idea.plugin.BaseXLangPluginTestCase;
+import io.nop.idea.plugin.lang.script.XLangScriptFileType;
+
+/**
+ * @author flytreeleft
+ * @date 2025-07-06
+ */
+public class TestXLangScriptRename extends BaseXLangPluginTestCase {
+    private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension();
+
+    public void testRenameVar() {
+        assertRename("data", """
+                let abc = 123;
+                const def = abc + 1;
+                """, """
+                             let data = 123;
+                             const def = data + 1;
+                             """);
+    }
+
+    public void testRenameJava() {
+        assertJavaRename("name", """
+                package io.nop.xlang.xdef;
+                public class Sample {
+                    public void getName() {}
+                }
+                """, """
+                                 import io.nop.xlang.xdef.Sample;
+                                 let s = new Sample();
+                                 s.getName();
+                                 """, """
+                                 import io.nop.xlang.xdef.Sample;
+                                 let s = new Sample();
+                                 s.name();
+                                 """);
+        assertJavaRename("name", """
+                package io.nop.xlang.xdef;
+                public class Sample {
+                    public void getName() {}
+                }
+                """, """
+                                 let s = new io.nop.xlang.xdef.Sample();
+                                 s.getName();
+                                 """, """
+                                 let s = new io.nop.xlang.xdef.Sample();
+                                 s.name();
+                                 """);
+
+        assertJavaRename("Sample123", """
+                package io.nop.xlang.xdef;
+                public class Sample { }
+                """, """
+                                 import io.nop.xlang.xdef.Sample;
+                                 let s = new Sample();
+                                 """, """
+                                 import io.nop.xlang.xdef.Sample123;
+                                 let s = new Sample123();
+                                 """);
+        assertJavaRename("Sample456", """
+                package io.nop.xlang.xdef;
+                public class Sample { }
+                """, """
+                                 let s = new io.nop.xlang.xdef.Sample();
+                                 """, """
+                                 let s = new io.nop.xlang.xdef.Sample456();
+                                 """);
+        assertJavaRename("Sample789", """
+                package io.nop.xlang.xdef;
+                public class Sample { }
+                """, """
+                                 let s = new io.nop.xlang.xdef. Sample();
+                                 """, """
+                                 let s = new io.nop.xlang.xdef. Sample789();
+                                 """);
+
+        assertJavaRename("username", """
+                package io.nop.xlang.xdef;
+                public class Sample { public final String name; }
+                """, """
+                                 import io.nop.xlang.xdef.Sample;
+                                 let s = new Sample();
+                                 let name = s.name;
+                                 """, """
+                                 import io.nop.xlang.xdef.Sample;
+                                 let s = new Sample();
+                                 let name = s.username;
+                                 """);
+        assertJavaRename("username", """
+                package io.nop.xlang.xdef;
+                public class Sample { public final String name; }
+                """, """
+                                 let s = new io.nop.xlang.xdef.Sample();
+                                 let name = s.name;
+                                 """, """
+                                 let s = new io.nop.xlang.xdef.Sample();
+                                 let name = s.username;
+                                 """);
+
+        assertJavaRename("xpl", """
+                package io.nop.xlang.xdef;
+                public class Sample { }
+                """, """
+                                 import io.nop.xlang.xdef.Sample;
+                                 let s = new io.nop.xlang.xdef.Sample();
+                                 """, """
+                                 import io.nop.xpl.xdef.Sample;
+                                 let s = new io.nop.xpl.xdef.Sample();
+                                 """);
+    }
+
+    protected void assertRename(String newName, String text, String expectedText) {
+        myFixture.configureByText("sample." + ext, text);
+        myFixture.renameElementAtCaret(newName);
+
+        myFixture.checkResult(expectedText);
+    }
+
+    protected void assertJavaRename(String newName, String javaText, String sampleText, String expectedText) {
+        PsiFile testFile = myFixture.configureByText("sample." + ext, sampleText);
+
+        myFixture.configureByText("Sample.java", javaText);
+        myFixture.renameElementAtCaret(newName);
+
+        assertEquals(expectedText, testFile.getText());
+    }
+}
-- 
Gitee


From c5f6b7611a5578830bec25b47cb1f62b8fd45952 Mon Sep 17 00:00:00 2001
From: flytreeleft 
Date: Mon, 7 Jul 2025 13:00:40 +0800
Subject: [PATCH 40/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?=
 =?UTF-8?q?=E5=AF=B9=20XLang=20Script=20=E4=B8=AD=E7=9A=84=E5=8F=98?=
 =?UTF-8?q?=E9=87=8F=E3=80=81=E5=87=BD=E6=95=B0=E3=80=81=E5=87=BD=E6=95=B0?=
 =?UTF-8?q?=E5=8F=82=E6=95=B0=E8=BF=9B=E8=A1=8C=E9=87=8D=E5=91=BD=E5=90=8D?=
 =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=AF=B9=E5=85=B6=E8=A2=AB=E5=BC=95=E7=94=A8?=
 =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E7=9A=84=E5=90=8D=E5=AD=97=E4=B9=9F=E5=90=8C?=
 =?UTF-8?q?=E6=AD=A5=E8=BF=9B=E8=A1=8C=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ...r.java => XLangScriptNodeManipulator.java} |  33 +--
 .../XLangScriptNodeRenameProcessor.java       |  68 ++++++
 .../lang/script/psi/IdentifierNode.java       |  23 +-
 .../script/reference/IdentifierReference.java |   4 +
 .../reference/QualifiedNameReference.java     |   3 +-
 .../src/main/resources/META-INF/plugin.xml    |   3 +-
 .../plugin/lang/TestXLangScriptRename.java    | 201 ++++++++++++++++++
 .../test/resources/_vfs/test/reference/a.xlib |   2 +
 8 files changed, 320 insertions(+), 17 deletions(-)
 rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/{XLangScriptElementManipulator.java => XLangScriptNodeManipulator.java} (74%)
 create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeRenameProcessor.java

diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptElementManipulator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeManipulator.java
similarity index 74%
rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptElementManipulator.java
rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeManipulator.java
index 3b725ac5e..f436b2e5f 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptElementManipulator.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeManipulator.java
@@ -23,8 +23,9 @@ import org.jetbrains.annotations.Nullable;
  * @author flytreeleft
  * @date 2025-07-06
  */
-public class XLangScriptElementManipulator extends AbstractElementManipulator {
+public class XLangScriptNodeManipulator extends AbstractElementManipulator {
 
+    /** 对 element 采取就地更新策略 */
     @Override
     public @Nullable RuleSpecNode handleContentChange(
             @NotNull RuleSpecNode element, @NotNull TextRange rangeInElement, String newContent
@@ -36,19 +37,9 @@ public class XLangScriptElementManipulator extends AbstractElementManipulator
+ * 默认的重命名处理器 {@link RenamePsiElementProcessor#DEFAULT}
+ * 会调用元素的 {@link PsiNamedElement#setName(String)}
+ * 方法完成对重命名节点本身的修改,但不会查找并修改关联方,需要在
+ * {@link #prepareRenaming} 中完成对关联方的收集
+ *
+ * @author flytreeleft
+ * @date 2025-07-07
+ */
+public class XLangScriptNodeRenameProcessor extends RenamePsiElementProcessor {
+
+    @Override
+    public boolean canProcessElement(@NotNull PsiElement element) {
+        // Note: 暂时仅针对标志符节点的名字修改
+        return element instanceof IdentifierNode;
+    }
+
+    @Override
+    public void prepareRenaming(
+            @NotNull PsiElement element, @NotNull String newName, @NotNull Map allRenames,
+            @NotNull SearchScope scope
+    ) {
+        // TODO thisObj.invoke('doDeleteByQuery', ...) 中的第一个参数名需跟随当前 xlib 的函数标签名联动修改
+        ProgramNode root = PsiTreeUtil.getParentOfType(element, ProgramNode.class);
+        if (root == null) {
+            return;
+        }
+
+        // Note: 在 #canProcessElement 限定了仅针对 IdentifierNode,
+        // 故而,这里仅检查对 IdentifierNode 的引用的节点
+        PsiElement target = element;
+        PsiElementVisitor visitor = new PsiElementVisitor() {
+            @Override
+            public void visitElement(@NotNull PsiElement element) {
+                PsiReference[] refs = element.getReferences();
+                for (PsiReference ref : refs) {
+                    if (ref instanceof IdentifierReference idRef) {
+                        if (idRef.resolve() == target) {
+                            allRenames.put(idRef.getIdentifier(), newName);
+                        }
+                    }
+                }
+
+                element.acceptChildren(this);
+            }
+        };
+
+        root.acceptChildren(visitor);
+    }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java
index 8c2f32bca..6ca490e37 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java
@@ -1,7 +1,13 @@
 package io.nop.idea.plugin.lang.script.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.ElementManipulator;
+import com.intellij.psi.ElementManipulators;
 import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import com.intellij.util.IncorrectOperationException;
 import io.nop.commons.util.StringHelper;
 import io.nop.idea.plugin.lang.XLangVarDecl;
 import io.nop.idea.plugin.utils.PsiClassHelper;
@@ -19,12 +25,27 @@ import org.jetbrains.annotations.NotNull;
  * @author flytreeleft
  * @date 2025-07-02
  */
-public class IdentifierNode extends RuleSpecNode {
+public class IdentifierNode extends RuleSpecNode implements PsiNamedElement {
 
     public IdentifierNode(@NotNull ASTNode node) {
         super(node);
     }
 
+    /** Note: 只有返回非 null 值,才支持在 idea 中重命名该节点 */
+    @Override
+    public String getName() {
+        return getText();
+    }
+
+    @Override
+    public PsiElement setName(@NotNull String name) throws IncorrectOperationException {
+        ElementManipulator manipulator = ElementManipulators.getManipulator(this);
+
+        TextRange textRange = getTextRangeInParent().shiftLeft(getStartOffsetInParent());
+
+        return manipulator.handleContentChange(this, textRange, name);
+    }
+
     /** 获取变量的类型 */
     public PsiClass getVarType() {
         XLangVarDecl varDecl = getVarDecl();
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java
index e1fe21d6c..598454d85 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java
@@ -23,6 +23,10 @@ public class IdentifierReference extends XLangReferenceBase {
         this.identifier = identifier;
     }
 
+    public IdentifierNode getIdentifier() {
+        return this.identifier;
+    }
+
     @Override
     public @Nullable PsiElement resolveInner() {
         if (!identifier.isValid()) {
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
index 5ed325ad0..986ba5866 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
@@ -7,7 +7,6 @@ import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiPackage;
 import com.intellij.util.IncorrectOperationException;
 import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
-import io.nop.idea.plugin.lang.script.psi.Identifier;
 import io.nop.idea.plugin.lang.script.psi.IdentifierNode;
 import io.nop.idea.plugin.utils.PsiClassHelper;
 import org.jetbrains.annotations.NotNull;
@@ -81,7 +80,7 @@ public class QualifiedNameReference extends XLangReferenceBase {
         TextRange rangeInElement = getRangeInElement();
 
         // 直接替换名字
-        ((Identifier) identifier.getFirstChild()).replaceWithText(newElementName);
+        identifier.setName(newElementName);
 
         rangeInElement = identifier.getTextRangeInParent().shiftRight(rangeInElement.getStartOffset());
         setRangeInElement(rangeInElement);
diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml
index 1ec768cdf..e81408f6b 100644
--- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml
+++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml
@@ -119,7 +119,8 @@
                           implementationClass="io.nop.idea.plugin.lang.script.XLangScriptASTFactory"/>
         
         
+                                 implementationClass="io.nop.idea.plugin.lang.script.XLangScriptNodeManipulator"/>
+        
         
diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java
index fe5ef96c2..40db9db12 100644
--- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java
+++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java
@@ -22,9 +22,210 @@ public class TestXLangScriptRename extends BaseXLangPluginTestCase {
         assertRename("data", """
                 let abc = 123;
                 const def = abc + 1;
+                let s = abc + ' abc';
                 """, """
                              let data = 123;
                              const def = data + 1;
+                             let s = data + ' abc';
+                             """);
+        assertRename("data", """
+                let abc = 123;
+                const def = abc + 1;
+                let s = abc + ' abc';
+                """, """
+                             let data = 123;
+                             const def = data + 1;
+                             let s = data + ' abc';
+                             """);
+
+        assertRename("data", """
+                let abc = 'abc';
+                abc.trim();
+                abc.isEmpty();
+                """, """
+                             let data = 'abc';
+                             data.trim();
+                             data.isEmpty();
+                             """);
+        assertRename("data", """
+                let abc = 'abc';
+                abc.trim();
+                abc.isEmpty();
+                """, """
+                             let data = 'abc';
+                             data.trim();
+                             data.isEmpty();
+                             """);
+
+        assertRename("data", """
+                let abc = 'abc';
+                let def = {abc, def: 123};
+                """, """
+                             let data = 'abc';
+                             let def = {data, def: 123};
+                             """);
+        assertRename("data", """
+                let abc = 'abc';
+                let def = {abc, def: 123};
+                """, """
+                             let data = 'abc';
+                             let def = {data, def: 123};
+                             """);
+        assertRename("data", """
+                let abc = 'abc';
+                let def = {abc: abc, def: 123};
+                """, """
+                             let data = 'abc';
+                             let def = {abc: data, def: 123};
+                             """);
+        assertRename("data", """
+                let abc = 'abc';
+                let def = {abc: abc, def: 123};
+                """, """
+                             let data = 'abc';
+                             let def = {abc: data, def: 123};
+                             """);
+    }
+
+    public void testRenameFunction() {
+        assertRename("fn_1", """
+                function fn(a, b) {}
+                const r = fn(1, 2);
+                """, """
+                             function fn_1(a, b) {}
+                             const r = fn_1(1, 2);
+                             """);
+        assertRename("fn_1", """
+                function fn(a, b) {}
+                const r = fn(1, 2);
+                """, """
+                             function fn_1(a, b) {}
+                             const r = fn_1(1, 2);
+                             """);
+
+        assertRename("fn_1", """
+                const fn = (a, b) => {};
+                const r = fn(1, 2);
+                """, """
+                             const fn_1 = (a, b) => {};
+                             const r = fn_1(1, 2);
+                             """);
+        assertRename("fn_1", """
+                const fn = (a, b) => {};
+                const r = fn(1, 2);
+                """, """
+                             const fn_1 = (a, b) => {};
+                             const r = fn_1(1, 2);
+                             """);
+
+        assertRename("aa", """
+                function fn(a1, b1) { return a1 + b1; }
+                """, """
+                             function fn(aa, b1) { return aa + b1; }
+                             """);
+        assertRename("aa", """
+                function fn(a1, b1) { return a1 + b1; }
+                """, """
+                             function fn(aa, b1) { return aa + b1; }
+                             """);
+        assertRename("bb", """
+                function fn(a1, b1) { return a1 + b1; }
+                """, """
+                             function fn(a1, bb) { return a1 + bb; }
+                             """);
+        assertRename("bb", """
+                function fn(a1, b1) { return a1 + b1; }
+                """, """
+                             function fn(a1, bb) { return a1 + bb; }
+                             """);
+
+        assertRename("aa", """
+                const fn = (a1, b1) => a1 + b1;
+                """, """
+                             const fn = (aa, b1) => aa + b1;
+                             """);
+        assertRename("aa", """
+                const fn = (a1, b1) => a1 + b1;
+                """, """
+                             const fn = (aa, b1) => aa + b1;
+                             """);
+        assertRename("bb", """
+                const fn = (a1, b1) => a1 + b1;
+                """, """
+                             const fn = (a1, bb) => a1 + bb;
+                             """);
+        assertRename("bb", """
+                const fn = (a1, b1) => a1 + b1;
+                """, """
+                             const fn = (a1, bb) => a1 + bb;
+                             """);
+
+        assertRename("aa", """
+                function fn(a1, b1) {
+                    const c = a1 + 1;
+                    return c + a1 + b1;
+                }
+                """, """
+                             function fn(aa, b1) {
+                                 const c = aa + 1;
+                                 return c + aa + b1;
+                             }
+                             """);
+        assertRename("aa", """
+                function fn(a1, b1) {
+                    const c = a1 + 1;
+                    return c + a1 + b1;
+                }
+                """, """
+                             function fn(aa, b1) {
+                                 const c = aa + 1;
+                                 return c + aa + b1;
+                             }
+                             """);
+        assertRename("aa", """
+                function fn(a1, b1) {
+                    const c = a1 + 1;
+                    return c + a1 + b1;
+                }
+                """, """
+                             function fn(aa, b1) {
+                                 const c = aa + 1;
+                                 return c + aa + b1;
+                             }
+                             """);
+
+        assertRename("aa", """
+                const fn = (a1, b1) => {
+                    const c = a1 + 1;
+                    return c + a1 + b1;
+                };
+                """, """
+                             const fn = (aa, b1) => {
+                                 const c = aa + 1;
+                                 return c + aa + b1;
+                             };
+                             """);
+        assertRename("aa", """
+                const fn = (a1, b1) => {
+                    const c = a1 + 1;
+                    return c + a1 + b1;
+                };
+                """, """
+                             const fn = (aa, b1) => {
+                                 const c = aa + 1;
+                                 return c + aa + b1;
+                             };
+                             """);
+        assertRename("aa", """
+                const fn = (a1, b1) => {
+                    const c = a1 + 1;
+                    return c + a1 + b1;
+                };
+                """, """
+                             const fn = (aa, b1) => {
+                                 const c = aa + 1;
+                                 return c + aa + b1;
+                             };
                              """);
     }
 
diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib
index b998d9262..1a502e966 100644
--- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib
+++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib
@@ -13,6 +13,7 @@
             
                 
Date: Mon, 7 Jul 2025 18:12:07 +0800
Subject: [PATCH 41/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?=
 =?UTF-8?q?=E5=AF=B9=20XLang=20Script=20=E4=B8=AD=E7=9A=84=E5=AF=B9?=
 =?UTF-8?q?=E8=B1=A1=E6=88=90=E5=91=98=E7=9A=84=E4=BB=A3=E7=A0=81=E8=A1=A5?=
 =?UTF-8?q?=E5=85=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../lang/script/psi/ExpressionNode.java       | 11 ++-
 .../lang/script/psi/IdentifierNode.java       |  2 +-
 .../script/psi/ImportDeclarationNode.java     |  2 +-
 .../plugin/lang/script/psi/RuleSpecNode.java  |  4 +-
 .../psi/TypeNameNodePredefinedNode.java       |  2 +-
 .../reference/ObjectMemberReference.java      | 99 +++++++++++++++++++
 .../reference/PredefinedTypeReference.java    |  9 +-
 .../reference/QualifiedNameReference.java     | 54 +++++++++-
 .../plugin/resource/ProjectDictProvider.java  |  5 +-
 .../nop/idea/plugin/utils/PsiClassHelper.java | 46 +++++++--
 .../lang/TestXLangScriptCompletions.java      | 58 +++++++++++
 .../plugin/lang/TestXLangScriptParser.java    | 20 ----
 .../_vfs/test/java/XJsonDomainHandler.java    |  1 +
 .../test/resources/_vfs/test/reference/a.xlib |  3 +
 14 files changed, 269 insertions(+), 47 deletions(-)

diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java
index a421430c6..0a6df6e9f 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java
@@ -455,6 +455,13 @@ public class ExpressionNode extends RuleSpecNode {
         return getFirstChild() instanceof ArrayExpressionNode;
     }
 
+    /** 获取对象的 class */
+    public PsiClass getObjectClass() {
+        ExpressionNode obj = (ExpressionNode) getFirstChild();
+
+        return obj.getResultType();
+    }
+
     /** 获取对象的方法 */
     public PsiMethod getObjectMethod() {
         PsiMethod[] methods = getObjectMethods();
@@ -496,9 +503,7 @@ public class ExpressionNode extends RuleSpecNode {
     }
 
     protected  T getObjectMember(BiFunction consumer, T defaultValue) {
-        ExpressionNode obj = (ExpressionNode) getFirstChild();
-        PsiClass objClass = obj.getResultType();
-
+        PsiClass objClass = getObjectClass();
         if (objClass == null) {
             return defaultValue;
         }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java
index 6ca490e37..df8c104c0 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java
@@ -64,7 +64,7 @@ public class IdentifierNode extends RuleSpecNode implements PsiNamedElement {
             && StringHelper.isValidClassName(varName) //
         ) {
             // Note: java.lang 中的类不需要显式导入
-            PsiClass clazz = PsiClassHelper.findClass(getProject(), "java.lang." + varName);
+            PsiClass clazz = PsiClassHelper.findClass(this, "java.lang." + varName);
             if (clazz != null) {
                 decl = new XLangVarDecl(clazz, clazz);
             }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java
index 3bc979a75..835439d8c 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java
@@ -51,7 +51,7 @@ public class ImportDeclarationNode extends RuleSpecNode {
         }
 
         String classFQN = qualifiedName.getFullyName();
-        PsiClass clazz = PsiClassHelper.findClass(getProject(), classFQN);
+        PsiClass clazz = PsiClassHelper.findClass(this, classFQN);
         if (clazz == null) {
             return Map.of();
         }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java
index 3ad35504c..3a1ebcd91 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java
@@ -93,7 +93,7 @@ public class RuleSpecNode extends ASTWrapperPsiElement implements XLangVarScope
     }
 
     protected PsiClass getPsiClassByPsiType(PsiType type) {
-        return PsiClassHelper.getTypeClass(getProject(), type);
+        return PsiClassHelper.getTypeClass(this, type);
     }
 
     protected PsiClass getPsiClassByToken(TokenIElementType token) {
@@ -110,6 +110,6 @@ public class RuleSpecNode extends ASTWrapperPsiElement implements XLangVarScope
         } else if (TOKEN_literal_string.contains(token)) {
             clazz = String.class;
         }
-        return clazz != null ? PsiClassHelper.findClass(getProject(), clazz.getName()) : null;
+        return clazz != null ? PsiClassHelper.findClass(this, clazz.getName()) : null;
     }
 }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java
index 2438cad6a..07d566ee4 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java
@@ -32,7 +32,7 @@ public class TypeNameNodePredefinedNode extends RuleSpecNode {
     public PsiClass getPredefinedType() {
         String typeName = getTypeName().getText();
 
-        return PredefinedTypeReference.getPredefinedType(getProject(), typeName);
+        return PredefinedTypeReference.getPredefinedType(this, typeName);
     }
 
     @Override
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java
index 0ea7e2357..7189b964a 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java
@@ -1,14 +1,36 @@
 package io.nop.idea.plugin.lang.script.reference;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import com.intellij.codeInsight.completion.InsertHandler;
+import com.intellij.codeInsight.completion.JavaLookupElementBuilder;
+import com.intellij.codeInsight.completion.PrioritizedLookupElement;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
 import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.CommonClassNames;
 import com.intellij.psi.ElementManipulator;
+import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiMember;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil;
+import com.intellij.psi.util.MethodSignatureBackedByPsiMethod;
 import com.intellij.util.IncorrectOperationException;
 import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
 import io.nop.idea.plugin.lang.script.psi.ExpressionNode;
+import one.util.streamex.StreamEx;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil.getMethodSignature;
+import static com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil.isClassWithName;
+
 /**
  * 对象成员(属性或方法)引用
  *
@@ -44,4 +66,81 @@ public class ObjectMemberReference extends XLangReferenceBase {
 
         return element;
     }
+
+    @Override
+    public Object @NotNull [] getVariants() {
+        // Note: 只有在当前引用的结果不存在时,才需要补全,而其可补全项由上层引用结果确定
+        PsiClass clazz = ((ExpressionNode) myElement).getObjectClass();
+        if (clazz == null) {
+            return LookupElement.EMPTY_ARRAY;
+        }
+
+        List result = new ArrayList<>();
+
+        // 代码来自 JavaLangClassMemberReference.getVariants
+        LookupElement[] fields = StreamEx.of(clazz.getAllFields())
+                                         .filter(field -> isPotentiallyAccessible(field, clazz))
+                                         .distinct(PsiField::getName)
+                                         .sorted(Comparator.comparingInt((PsiField field) -> isPublic(field) ? 0 : 1)
+                                                           .thenComparing(PsiField::getName))
+                                         .map(field -> withPriority(JavaLookupElementBuilder.forField(field),
+                                                                    isPublic(field)))
+                                         .toArray(LookupElement[]::new);
+        Collections.addAll(result, fields);
+
+        LookupElement[] methods = StreamEx.of(clazz.getVisibleSignatures())
+                                          .map(MethodSignatureBackedByPsiMethod::getMethod)
+                                          .filter(method -> isRegularMethod(method) && isPotentiallyAccessible(method,
+                                                                                                               clazz))
+                                          .sorted(Comparator.comparingInt(ObjectMemberReference::getMethodSortOrder)
+                                                            .thenComparing(PsiMethod::getName))
+                                          .map(method -> withPriority(lookupMethod(method, null),
+                                                                      -getMethodSortOrder(method)))
+                                          .nonNull()
+                                          .toArray(LookupElement[]::new);
+        Collections.addAll(result, methods);
+
+        return result.toArray();
+    }
+
+    // <<<<<<<<<<<<<<<< 代码来自 JavaLangClassMemberReference.getVariants
+    static boolean isPotentiallyAccessible(PsiMember member, PsiClass clazz) {
+        return member != null && (member.getContainingClass() == clazz || isPublic(member));
+    }
+
+    static boolean isPublic(@NotNull PsiMember member) {
+        return member.hasModifierProperty(PsiModifier.PUBLIC);
+    }
+
+    @NotNull
+    static LookupElement withPriority(@NotNull LookupElement lookupElement, boolean hasPriority) {
+        return hasPriority ? lookupElement : PrioritizedLookupElement.withPriority(lookupElement, -1);
+    }
+
+    static LookupElement withPriority(@Nullable LookupElement lookupElement, int priority) {
+        return priority == 0 || lookupElement == null
+               ? lookupElement
+               : PrioritizedLookupElement.withPriority(lookupElement, priority);
+    }
+
+    static boolean isRegularMethod(@Nullable PsiMethod method) {
+        return method != null && !method.isConstructor();
+    }
+
+    static int getMethodSortOrder(@NotNull PsiMethod method) {
+        return isJavaLangObject(method.getContainingClass()) ? 1 : isPublic(method) ? -1 : 0;
+    }
+
+    static boolean isJavaLangObject(@Nullable PsiClass aClass) {
+        return isClassWithName(aClass, CommonClassNames.JAVA_LANG_OBJECT);
+    }
+
+    static LookupElement lookupMethod(@NotNull PsiMethod method, @Nullable InsertHandler insertHandler) {
+        final JavaReflectionReferenceUtil.ReflectiveSignature signature = getMethodSignature(method);
+        return signature != null ? LookupElementBuilder.create(signature, method.getName())
+                                                       .withIcon(signature.getIcon())
+                                                       .withTailText(signature.getShortArgumentTypes())
+                                                       .withInsertHandler(insertHandler) : null;
+    }
+    // >>>>>>>>>>>>>>>>>>>>>
 }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java
index 1946959a6..6bad9c688 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java
@@ -1,6 +1,5 @@
 package io.nop.idea.plugin.lang.script.reference;
 
-import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.TextRange;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElement;
@@ -29,12 +28,10 @@ public class PredefinedTypeReference extends XLangReferenceBase {
             return null;
         }
 
-        Project project = myElement.getProject();
-
-        return getPredefinedType(project, typeName.getText());
+        return getPredefinedType(myElement, typeName.getText());
     }
 
-    public static PsiClass getPredefinedType(Project project, String typeName) {
+    public static PsiClass getPredefinedType(PsiElement context, String typeName) {
         // 仅包含确定类型,详见 nop-xlang/model/antlr/XLangTypeSystem.g4
         Class typeClass = switch (typeName) {
             case "any" -> Object.class;
@@ -46,6 +43,6 @@ public class PredefinedTypeReference extends XLangReferenceBase {
             default -> null;
         };
 
-        return typeClass != null ? PsiClassHelper.findClass(project, typeClass.getName()) : null;
+        return typeClass != null ? PsiClassHelper.findClass(context, typeClass.getName()) : null;
     }
 }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
index 986ba5866..6a970f3d4 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
@@ -1,11 +1,23 @@
 package io.nop.idea.plugin.lang.script.reference;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import com.intellij.codeInsight.completion.JavaClassNameCompletionContributor;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Iconable;
 import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiNameHelper;
 import com.intellij.psi.PsiPackage;
+import com.intellij.psi.search.GlobalSearchScope;
 import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.containers.ContainerUtil;
 import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
 import io.nop.idea.plugin.lang.script.psi.IdentifierNode;
 import io.nop.idea.plugin.utils.PsiClassHelper;
@@ -35,7 +47,8 @@ public class QualifiedNameReference extends XLangReferenceBase {
             return null;
         }
 
-        Project project = myElement.getProject();
+        PsiElement context = myElement;
+        Project project = context.getProject();
         String subName = identifier.getText();
 
         // 最顶层标志符
@@ -66,7 +79,7 @@ public class QualifiedNameReference extends XLangReferenceBase {
                 }
 
                 // 若包不存在,则可能是类
-                return PsiClassHelper.findClass(project, subName);
+                return PsiClassHelper.findClass(context, subName);
             }
         }
 
@@ -98,4 +111,41 @@ public class QualifiedNameReference extends XLangReferenceBase {
 
         return newName != null ? handleElementRename(newName) : null;
     }
+
+    @Override
+    public Object @NotNull [] getVariants() {
+        // Note: 只有在当前引用的结果不存在时,才需要补全,而其可补全项由上层引用结果确定
+        PsiElement context = parentReference != null ? parentReference.resolve() : null;
+        if (context == null) {
+            return LookupElement.EMPTY_ARRAY;
+        }
+
+        List result = new ArrayList<>();
+        GlobalSearchScope scope = PsiClassHelper.getSearchScope(context);
+
+        if (context instanceof PsiPackage pkg) {
+            String pkgName = pkg.getQualifiedName();
+            for (PsiPackage subPkg : pkg.getSubPackages(scope)) {
+                String shortName = subPkg.getQualifiedName().substring(pkgName.length());
+
+                if (PsiNameHelper.getInstance(subPkg.getProject()).isIdentifier(shortName)) {
+                    result.add(LookupElementBuilder.create(subPkg)
+                                                   .withIcon(subPkg.getIcon(Iconable.ICON_FLAG_VISIBILITY)));
+                }
+            }
+
+            List classes = ContainerUtil.filter(pkg.getClasses(scope),
+                                                          clazz -> StringUtil.isNotEmpty(clazz.getName()));
+            for (PsiClass clazz : classes) {
+                result.add(JavaClassNameCompletionContributor.createClassLookupItem(clazz, false));
+            }
+        } //
+        else if (context instanceof PsiClass clazz) {
+            List classes = ContainerUtil.filter(clazz.getInnerClasses(),
+                                                          c -> c.hasModifierProperty(PsiModifier.STATIC));
+            result.addAll(classes);
+        }
+
+        return result.toArray();
+    }
 }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java
index de49fc1d8..e38eca98e 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java
@@ -14,6 +14,7 @@ import com.intellij.lang.jvm.JvmEnumField;
 import com.intellij.openapi.project.Project;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiField;
+import com.intellij.psi.search.GlobalSearchScope;
 import io.nop.api.core.annotations.core.Description;
 import io.nop.api.core.annotations.core.Label;
 import io.nop.api.core.annotations.core.Option;
@@ -69,7 +70,9 @@ public class ProjectDictProvider implements IDictProvider {
         }
         // 从枚举类中得到字典信息
         else if (dictName.indexOf('.') > 0 && StringHelper.isValidClassName(dictName)) {
-            PsiClass clazz = PsiClassHelper.findClass(project, dictName);
+            GlobalSearchScope scope = GlobalSearchScope.allScope(project);
+
+            PsiClass clazz = PsiClassHelper.findClass(project, dictName, scope);
             if (clazz == null) {
                 return null;
             }
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java
index 169e85a49..ee103ab68 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java
@@ -6,6 +6,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
 import com.intellij.openapi.project.Project;
 import com.intellij.psi.CommonClassNames;
 import com.intellij.psi.JavaPsiFacade;
@@ -74,6 +76,20 @@ public class PsiClassHelper {
         javaClassRefProvider.setOption(JavaClassReferenceProvider.ADVANCED_RESOLVE, true);
     }
 
+    public static @NotNull GlobalSearchScope getSearchScope(@NotNull PsiElement element) {
+        Project project = element.getProject();
+
+        GlobalSearchScope scope = javaClassRefProvider.getScope(project);
+        if (scope == null) {
+            Module module = ModuleUtilCore.findModuleForPsiElement(element);
+
+            scope = module == null
+                    ? GlobalSearchScope.allScope(project)
+                    : module.getModuleWithDependenciesAndLibrariesScope(true);
+        }
+        return scope;
+    }
+
     public static PsiReference @NotNull [] createJavaClassReferences(
             PsiElement element, String qualifiedName, int startInElement
     ) {
@@ -87,17 +103,18 @@ public class PsiClassHelper {
     }
 
     /** 得到 {@link PsiType} 对应的 {@link PsiClass} */
-    public static PsiClass getTypeClass(Project project, PsiType type) {
+    public static PsiClass getTypeClass(PsiElement context, PsiType type) {
         if (type == null) {
             return null;
         }
 
+        Project project = context.getProject();
         // 处理通配符泛型
         if (type instanceof PsiWildcardType t) {
             PsiType bound = t.getBound();
 
             return bound != null
-                   ? getTypeClass(project, bound)
+                   ? getTypeClass(context, bound)
                    : PsiUtil.resolveClassInType(PsiType.getJavaLangObject(t.getManager(), t.getResolveScope()));
         }
         // 处理类型参数
@@ -105,7 +122,7 @@ public class PsiClassHelper {
             PsiClassType[] bounds = t.getExtendsListTypes();
 
             if (bounds.length > 0) {
-                return getTypeClass(project, bounds[0]);
+                return getTypeClass(context, bounds[0]);
             }
             return PsiUtil.resolveClassInType(PsiType.getJavaLangObject(t.getManager(), t.getResolveScope()));
         }
@@ -114,13 +131,13 @@ public class PsiClassHelper {
             String wrapperName = primitiveTypeWrapper.get(t);
 
             if (wrapperName != null) {
-                return JavaPsiFacade.getInstance(project).findClass(wrapperName, GlobalSearchScope.allScope(project));
+                return findClass(context, wrapperName);
             }
             return null;
         }
         // 处理数组类型
         else if (type instanceof PsiArrayType t) {
-            return getTypeClass(project, t.getComponentType());
+            return getTypeClass(context, t.getComponentType());
         }
         // 处理类类型(包括泛型)
         else if (type instanceof PsiClassType t) {
@@ -131,7 +148,7 @@ public class PsiClassHelper {
             if (clazz != null && parameters.length > 0) {
                 // List -> 返回 String.class
                 if (CommonClassNames.JAVA_UTIL_LIST.equals(clazz.getQualifiedName())) {
-                    return getTypeClass(project, parameters[0]);
+                    return getTypeClass(context, parameters[0]);
                 }
 
                 // 自定义泛型类
@@ -140,7 +157,7 @@ public class PsiClassHelper {
                     // 查找实际使用的类型参数
                     for (int i = 0; i < typeParams.length; i++) {
                         if (i < parameters.length) {
-                            PsiClass resolved = getTypeClass(project, parameters[i]);
+                            PsiClass resolved = getTypeClass(context, parameters[i]);
 
                             if (resolved != null) {
                                 return resolved;
@@ -182,8 +199,15 @@ public class PsiClassHelper {
         return map;
     }
 
-    public static PsiClass findClass(Project project, String className) {
-        return JavaPsiFacade.getInstance(project).findClass(className, GlobalSearchScope.allScope(project));
+    public static PsiClass findClass(PsiElement context, String className) {
+        Project project = context.getProject();
+        GlobalSearchScope scope = PsiClassHelper.getSearchScope(context);
+
+        return JavaPsiFacade.getInstance(project).findClass(className, scope);
+    }
+
+    public static PsiClass findClass(Project project, String className, GlobalSearchScope scope) {
+        return JavaPsiFacade.getInstance(project).findClass(className, scope);
     }
 
     public static PsiPackage findPackage(Project project, String pkgName) {
@@ -192,7 +216,9 @@ public class PsiClassHelper {
 
     /** 查找指定类的继承类 */
     public static @NotNull Query findInheritors(Project project, String className) {
-        PsiClass clazz = findClass(project, className);
+        GlobalSearchScope scope = GlobalSearchScope.allScope(project);
+
+        PsiClass clazz = findClass(project, className, scope);
         if (clazz == null) {
             return EmptyQuery.getEmptyQuery();
         }
diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
index 52868d1e7..b30998f20 100644
--- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
+++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
@@ -42,4 +42,62 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase {
             assertCompletion(expected);
         }
     }
+
+    public void testObjectMemberCompletion() {
+        assertCompletion("""
+                                 let s = "abc";
+                                 s.toCharA
+                                 """, """
+                                 let s = "abc";
+                                 s.toCharArray
+                                 """);
+
+        assertCompletion("""
+                                 const handler = new io.nop.xlang.xdef.domain.XJsonDomai
+                                 """, """
+                                 const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler
+                                 """);
+
+        assertCompletion("""
+                                 import io.nop.xlang.xdef.domain.XJsonDomainHandler;
+                                 const handler = XJsonDomainHandler.INST
+                                 """, """
+                                 import io.nop.xlang.xdef.domain.XJsonDomainHandler;
+                                 const handler = XJsonDomainHandler.INSTANCE
+                                 """);
+        assertCompletion("""
+                                 import io.nop.xlang.xdef.domain.XJsonDomainHandler;
+                                 const handler = new XJsonDomainHandler();
+                                 handler.instan
+                                 """, """
+                                 import io.nop.xlang.xdef.domain.XJsonDomainHandler;
+                                 const handler = new XJsonDomainHandler();
+                                 handler.instance
+                                 """);
+
+        assertCompletion("""
+                                 import io.nop.xlang.xdef.domain.XJsonDomainHandler;
+                                 const handler = new XJsonDomainHandler.Su
+                                 """, """
+                                 import io.nop.xlang.xdef.domain.XJsonDomainHandler;
+                                 const handler = new XJsonDomainHandler.Sub
+                                 """);
+        assertCompletion("""
+                                 import io.nop.xlang.xdef.domain.XJsonDomainHandler;
+                                 const handler = new XJsonDomainHandler.Sub();
+                                 handler.ag
+                                 """, """
+                                 import io.nop.xlang.xdef.domain.XJsonDomainHandler;
+                                 const handler = new XJsonDomainHandler.Sub();
+                                 handler.age
+                                 """);
+    }
+
+    /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */
+    protected void assertCompletion(String text, String expectedText) {
+        myFixture.configureByText("sample." + ext, text);
+        myFixture.completeBasic();
+
+        myFixture.checkResult(expectedText);
+    }
 }
diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java
index 2fef14d0c..69ca73544 100644
--- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java
+++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java
@@ -65,19 +65,6 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase {
                         "/test/ast/statement-1.ast");
     }
 
-    public void testJavaParseTree() {
-        assertJavaParseTree("""
-                                    public class Sample {
-                                        public static void main(String[] args) {
-                                            String s1 = StringHelper.trim(b);
-                                            String s2 = a.b.c(1, 2);
-                                            String s3 = a.b.c.e;
-                                            some.other.another.start();
-                                        }
-                                    }
-                                    """);
-    }
-
     protected void assertParseTree(String code, String expectedAstFile) {
         PsiFile testFile = myFixture.configureByText("sample." + ext, code);
         String testTree = toParseTreeText(testFile);
@@ -87,13 +74,6 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase {
         assertEquals(expectedTree, testTree);
     }
 
-    protected void assertJavaParseTree(String code) {
-        PsiFile testFile = myFixture.configureByText("Sample.java", code);
-        String testTree = toParseTreeText(testFile);
-
-        assertEquals("", testTree);
-    }
-
     protected String toParseTreeText(@NotNull PsiElement tree) {
         return DebugUtil.psiToString(tree, true, false);
     }
diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java
index 478d691df..c9862e161 100644
--- a/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java
+++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java
@@ -16,6 +16,7 @@ public class XJsonDomainHandler implements IStdDomainHandler {
 
     public static class Sub {
         private String name;
+        public final String age;
 
         public String getName() {
             return this.name;
diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib
index 1a502e966..0556c31b9 100644
--- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib
+++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib
@@ -19,12 +19,15 @@
                     const query = QueryBeanHelper.buildQueryBeanFromTreeBean(queryNode);
                     const ormTemplate = inject('nopOrmTemplate');
                     const mapper = ormTemplate.getRowMapper(rowType,false);
+                    const scanner = new io.nop.commons.text.tokenizer.TextScanner();
+                    const c = new java.lang.StringBuffer();
                     const s = "abc".startsWith("a");
                     const fn = (a: any, b: string) => a + b;
                     fn(1, 2);
                     Integer.valueOf;
                     Integer.valueOf('a');
                     PsiClassHelper.findClass();
+                    scanner.col;
 
                     while (true) {
                         let a = 's' instanceof string;
-- 
Gitee


From 20f66df847d6e8243ef651c898310ea85bd87e37 Mon Sep 17 00:00:00 2001
From: flytreeleft 
Date: Mon, 7 Jul 2025 18:29:03 +0800
Subject: [PATCH 42/82] =?UTF-8?q?nop-idea-pugin:=20=E6=9B=B4=E6=96=B0?=
 =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=A4=87=E6=B3=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java  | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java
index 477c77d83..6037a10df 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java
@@ -19,7 +19,8 @@ public class XLangScriptParserAdaptor extends ANTLRParserAdaptor {
 
     @Override
     protected ParseTree parse(Parser parser, IElementType root) {
-        // TODO 为 dot 节点之后的空白添加占位节点,以便于触发代码补全
+        // Note: 不需要为 dot 节点之后的空白添加占位节点,只要延迟到在
+        // PsiReference#resolve 中才获取引用元素,即可正常触发代码补全
         if (root instanceof IFileElementType) {
             return ((XLangParser) parser).program();
         }
-- 
Gitee


From d0f61e26e3fdc7b81f05f2d474932cf53959e39e Mon Sep 17 00:00:00 2001
From: flytreeleft 
Date: Mon, 7 Jul 2025 20:28:19 +0800
Subject: [PATCH 43/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AF=B9=20XLang=20Scr?=
 =?UTF-8?q?ipt=20=E7=9A=84=E5=8C=85=E5=90=8D=E8=A1=A5=E5=85=A8=E5=8F=AF?=
 =?UTF-8?q?=E9=80=89=E9=A1=B9=E8=BF=9B=E8=A1=8C=E6=8E=92=E5=BA=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../reference/QualifiedNameReference.java     | 46 ++++++++++++-------
 .../lang/TestXLangScriptCompletions.java      |  5 ++
 2 files changed, 35 insertions(+), 16 deletions(-)

diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
index 6a970f3d4..5acac3fba 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java
@@ -1,6 +1,8 @@
 package io.nop.idea.plugin.lang.script.reference;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 import com.intellij.codeInsight.completion.JavaClassNameCompletionContributor;
@@ -17,10 +19,10 @@ import com.intellij.psi.PsiNameHelper;
 import com.intellij.psi.PsiPackage;
 import com.intellij.psi.search.GlobalSearchScope;
 import com.intellij.util.IncorrectOperationException;
-import com.intellij.util.containers.ContainerUtil;
 import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
 import io.nop.idea.plugin.lang.script.psi.IdentifierNode;
 import io.nop.idea.plugin.utils.PsiClassHelper;
+import one.util.streamex.StreamEx;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -125,27 +127,39 @@ public class QualifiedNameReference extends XLangReferenceBase {
 
         if (context instanceof PsiPackage pkg) {
             String pkgName = pkg.getQualifiedName();
-            for (PsiPackage subPkg : pkg.getSubPackages(scope)) {
-                String shortName = subPkg.getQualifiedName().substring(pkgName.length());
 
-                if (PsiNameHelper.getInstance(subPkg.getProject()).isIdentifier(shortName)) {
-                    result.add(LookupElementBuilder.create(subPkg)
-                                                   .withIcon(subPkg.getIcon(Iconable.ICON_FLAG_VISIBILITY)));
-                }
-            }
+            LookupElement[] subPkgs = createPackageLookupItems(StreamEx.of(pkg.getSubPackages(scope)).filter(p -> {
+                String shortName = p.getQualifiedName().substring(pkgName.length() + 1);
 
-            List classes = ContainerUtil.filter(pkg.getClasses(scope),
-                                                          clazz -> StringUtil.isNotEmpty(clazz.getName()));
-            for (PsiClass clazz : classes) {
-                result.add(JavaClassNameCompletionContributor.createClassLookupItem(clazz, false));
-            }
+                return PsiNameHelper.getInstance(p.getProject()).isIdentifier(shortName);
+            }));
+            Collections.addAll(result, subPkgs);
+
+            LookupElement[] classes = createClassLookupItems(StreamEx.of(pkg.getClasses(scope))
+                                                                     .filter(clazz -> StringUtil.isNotEmpty(clazz.getName())));
+            Collections.addAll(result, classes);
         } //
         else if (context instanceof PsiClass clazz) {
-            List classes = ContainerUtil.filter(clazz.getInnerClasses(),
-                                                          c -> c.hasModifierProperty(PsiModifier.STATIC));
-            result.addAll(classes);
+            LookupElement[] classes = createClassLookupItems(StreamEx.of(clazz.getInnerClasses())
+                                                                     .filter(c -> c.hasModifierProperty(PsiModifier.STATIC)));
+            Collections.addAll(result, classes);
         }
 
         return result.toArray();
     }
+
+    protected LookupElement[] createPackageLookupItems(StreamEx stream) {
+        return stream.distinct(PsiPackage::getQualifiedName)
+                     .sorted(Comparator.comparing(PsiPackage::getQualifiedName))
+                     .map(p -> LookupElementBuilder.create(p).withIcon(p.getIcon(Iconable.ICON_FLAG_VISIBILITY)))
+                     .toArray(LookupElement[]::new);
+    }
+
+    protected LookupElement[] createClassLookupItems(StreamEx stream) {
+        return stream.filter(c -> c.getQualifiedName() != null)
+                     .distinct(PsiClass::getQualifiedName)
+                     .sorted(Comparator.comparing(PsiClass::getQualifiedName))
+                     .map(c -> JavaClassNameCompletionContributor.createClassLookupItem(c, false))
+                     .toArray(LookupElement[]::new);
+    }
 }
diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
index b30998f20..7eab9e158 100644
--- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
+++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
@@ -57,6 +57,11 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase {
                                  """, """
                                  const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler
                                  """);
+        assertCompletion("""
+                                 const handler = new io.nop.xlang.xdef.d
+                                 """, """
+                                 const handler = new io.nop.xlang.xdef.domain
+                                 """);
 
         assertCompletion("""
                                  import io.nop.xlang.xdef.domain.XJsonDomainHandler;
-- 
Gitee


From 0d76018aae5537cd70a0f45f6cf890a7911b4983 Mon Sep 17 00:00:00 2001
From: flytreeleft 
Date: Tue, 8 Jul 2025 16:57:11 +0800
Subject: [PATCH 44/82] =?UTF-8?q?nop-idea-pugin:=20=E8=A1=A5=E5=85=85=20XL?=
 =?UTF-8?q?ang=20Script=20=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../lang/TestXLangScriptCompletions.java      | 41 +++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
index 7eab9e158..40f991b42 100644
--- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
+++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java
@@ -98,6 +98,40 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase {
                                  """);
     }
 
+    public void testCompletionInXLib() {
+        assertCompletionInXLib("""
+                                       
+                                           
+                                               
+                                                   
+                                                       ();
+                                                       ]]>
+                                                   
+                                               
+                                           
+                                       
+                                       """, """
+                                       
+                                           
+                                               
+                                                   
+                                                       
+                                                   
+                                               
+                                           
+                                       
+                                       """);
+    }
+
     /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */
     protected void assertCompletion(String text, String expectedText) {
         myFixture.configureByText("sample." + ext, text);
@@ -105,4 +139,11 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase {
 
         myFixture.checkResult(expectedText);
     }
+
+    protected void assertCompletionInXLib(String text, String expectedText) {
+        configureByXLangText(text);
+        myFixture.completeBasic();
+
+        myFixture.checkResult(expectedText);
+    }
 }
-- 
Gitee


From 1e7bb351c830a6c5b41a0847d4e0b2b163605e4e Mon Sep 17 00:00:00 2001
From: flytreeleft 
Date: Tue, 8 Jul 2025 21:03:07 +0800
Subject: [PATCH 45/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?=
 =?UTF-8?q?=E5=AF=B9=20XmlASTFactory#createComposite=20=E9=87=8D=E5=86=99?=
 =?UTF-8?q?=EF=BC=8C=E4=BB=A5=E6=94=AF=E6=8C=81=E9=92=88=E5=AF=B9=20XLang?=
 =?UTF-8?q?=20=E5=88=9B=E5=BB=BA=E7=9B=B8=E5=BA=94=E7=9A=84=20PsiElement?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../plugin/lang/XLangParserDefinition.java    |  82 ++++++++++++-
 .../lang/XLangScriptLanguageInjector.java     |   3 +
 .../script/XLangScriptParserDefinition.java   |   2 +-
 .../idea/plugin/BaseXLangPluginTestCase.java  |  17 ++-
 .../nop/idea/plugin/lang/TestXLangParser.java |  28 +++++
 .../plugin/lang/TestXLangScriptParser.java    | 108 ++++++++----------
 .../test/resources/_vfs/test/ast/xlang-1.ast  |   0
 .../{statement-1.ast => xlang-script-1.ast}   |   0
 ...ement-err-1.ast => xlang-script-err-1.ast} |   0
 9 files changed, 176 insertions(+), 64 deletions(-)
 create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java
 create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast
 rename nop-idea-plugin/src/test/resources/_vfs/test/ast/{statement-1.ast => xlang-script-1.ast} (100%)
 rename nop-idea-plugin/src/test/resources/_vfs/test/ast/{statement-err-1.ast => xlang-script-err-1.ast} (100%)

diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java
index d2ed5247d..a86cfa498 100644
--- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java
@@ -7,19 +7,97 @@
  */
 package io.nop.idea.plugin.lang;
 
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.ParserDefinition;
+import com.intellij.lang.PsiParser;
 import com.intellij.lang.xml.XMLParserDefinition;
+import com.intellij.lang.xml.XmlASTFactory;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
 import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.tree.CompositeElement;
 import com.intellij.psi.impl.source.xml.XmlFileImpl;
+import com.intellij.psi.tree.IElementType;
 import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
 import org.jetbrains.annotations.NotNull;
 
-public class XLangParserDefinition extends XMLParserDefinition {
+/**
+ * 复用 XML 的解析能力,并对得到的 {@link PsiElement} 做 XLang 节点包装
+ * 

+ * 从 {@link com.intellij.lang.impl.PsiBuilderImpl#bind PsiBuilderImpl#bind} + * 可知,若 {@link ParserDefinition} 的实现类继承自 {@link com.intellij.lang.ASTFactory ASTFactory}, + * 则会直接由该实现类的方法 {@link #createComposite} 创建 {@link CompositeElement}, + * 否则,会采用 {@link com.intellij.psi.impl.source.parsing.xml.XmlParser XmlParser} + * 构造的 {@link com.intellij.lang.ASTNode#getElementType() ASTNode#getElementType()} 对应语言(始终为 + * {@link com.intellij.lang.xml.XMLLanguage XMLLanguage})所绑定的 + * {@link com.intellij.lang.ASTFactory ASTFactory} 来创建 {@link CompositeElement} + *

+ * 注:暂时没有其他简便的方案可以对 XML 解析器解析的 AST 节点指定 ASTFactory + * + * @author flytreeleft + * @date 2025-07-08 + */ +public class XLangParserDefinition extends XmlASTFactory implements ParserDefinition { static final IFileElementType XLANG_FILE = new IFileElementType(XLangLanguage.INSTANCE); @Override public @NotNull PsiFile createFile(@NotNull FileViewProvider viewProvider) { - // 解析XML文件,绑定到XLang语言类型。否则会报错 + // 解析 XML 文件,绑定到 XLang 语言类型。否则会报错 return new XmlFileImpl(viewProvider, XLANG_FILE); } + + @Override + public CompositeElement createComposite(@NotNull IElementType type) { + return super.createComposite(type); + } + + // <<<<<<<<<<<<<<<<<< 委派到 XMLParserDefinition,从而复用 xml 的 AST 解析逻辑 + + private final XMLParserDefinition xmlParserDefinition = new XMLParserDefinition(); + + /** Note: 只有在 {@link com.intellij.lang.ASTFactory ASTFactory} 中未创建 PsiElement 的节点才会调用该接口 */ + @Override + public @NotNull PsiElement createElement(ASTNode node) { + return xmlParserDefinition.createElement(node); + } + + @Override + public @NotNull Lexer createLexer(Project project) { + return xmlParserDefinition.createLexer(project); + } + + @Override + public @NotNull PsiParser createParser(Project project) { + return xmlParserDefinition.createParser(project); + } + + @Override + public @NotNull IFileElementType getFileNodeType() { + return xmlParserDefinition.getFileNodeType(); + } + + @Override + public @NotNull TokenSet getCommentTokens() { + return xmlParserDefinition.getCommentTokens(); + } + + @Override + public @NotNull TokenSet getStringLiteralElements() { + return xmlParserDefinition.getStringLiteralElements(); + } + + @Override + public @NotNull TokenSet getWhitespaceTokens() { + return xmlParserDefinition.getWhitespaceTokens(); + } + + @NotNull + @Override + public SpaceRequirements spaceExistenceTypeBetweenTokens(ASTNode left, ASTNode right) { + return xmlParserDefinition.spaceExistenceTypeBetweenTokens(left, right); + } + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>> } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java index c87c00dfd..c35df368d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java @@ -16,6 +16,9 @@ import org.jetbrains.annotations.NotNull; * XScript 脚本的 {@link LanguageInjector} *

* 用于识别脚本语言,以提供高亮、引用、自动补全等编码支持 + *

+ * 通过 InjectedLanguageManager.getInstance(host.getProject()).findInjectedElementAt(host, offset); + * 可以从目标元素内查找到已注入的其他语言节点 * * @author flytreeleft * @date 2025-06-25 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java index b74a198d1..dd6919bb6 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -77,6 +77,7 @@ public class XLangScriptParserDefinition implements ParserDefinition { return new XLangScriptParserAdaptor(); } + /** Note: 只有在 {@link com.intellij.lang.ASTFactory ASTFactory} 中未创建 PsiElement 的节点才会调用该接口 */ @NotNull @Override public PsiElement createElement(ASTNode node) { @@ -84,7 +85,6 @@ public class XLangScriptParserDefinition implements ParserDefinition { return new RuleSpecNode(node); } - // Note: 只有在 ASTFactory 中未创建 PsiElement 的节点才会调用该接口 return switch (rule.getRuleIndex()) { case XLangParser.RULE_program -> // new ProgramNode(node); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index c73f25d8a..c8b199243 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -24,7 +24,9 @@ import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.projectRoots.impl.JavaSdkImpl; import com.intellij.openapi.roots.ModuleRootModificationUtil; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.DebugUtil; import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; import io.nop.commons.lang.impl.Cancellable; @@ -87,8 +89,8 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur super.tearDown(); } - protected void configureByXLangText(String text) { - myFixture.configureByText("unit." + XLANG_EXT, text); + protected PsiFile configureByXLangText(String text) { + return myFixture.configureByText("unit." + XLANG_EXT, text); } protected void addAllNopXDefsToProject() { @@ -223,4 +225,15 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur // 验证结果 myFixture.checkResult(expectedText); } + + /** + * 检查 {@link PsiElement} 的解析树是否与指定的 vfs 文件 expectedAstFile 的内容相同 + */ + protected void assertASTTree(PsiElement tree, String expectedAstFile) { + String testTree = DebugUtil.psiToString(tree, true, false); + + String expectedTree = readVfsResource(expectedAstFile); + + assertEquals(expectedTree, testTree); + } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java new file mode 100644 index 000000000..284fae04d --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java @@ -0,0 +1,28 @@ +package io.nop.idea.plugin.lang; + +import com.intellij.psi.PsiFile; +import io.nop.idea.plugin.BaseXLangPluginTestCase; + +/** + * @author flytreeleft + * @date 2025-07-08 + */ +public class TestXLangParser extends BaseXLangPluginTestCase { + + public void testParseASTTree() { + assertASTTree(""" + + + + """, // + "/test/ast/xlang-1.ast"); + } + + protected void assertASTTree(String code, String expectedAstFile) { + PsiFile testFile = configureByXLangText(code); + + assertASTTree(testFile, expectedAstFile); + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index 69ca73544..836f0fb6d 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -1,11 +1,8 @@ package io.nop.idea.plugin.lang; -import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; -import com.intellij.psi.impl.DebugUtil; import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.idea.plugin.lang.script.XLangScriptFileType; -import org.jetbrains.annotations.NotNull; /** * @author flytreeleft @@ -15,66 +12,59 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); public void testParseStatement() { - assertParseTree(""" - import java.lang.; - const abc = ; - const abc = () =>; - """, "/test/ast/statement-err-1.ast"); + assertASTTree(""" + import java.lang.; + const abc = ; + const abc = () =>; + """, "/test/ast/xlang-script-err-1.ast"); - assertParseTree(""" - import java.lang.String; - import java.lang.Number; - import io.nop.xlang.xdef.domain.XJsonDomainHandler; - // - const abc = ormTemplate.findListByQuery(query, mapper); - // - a(1, 2); - a.b.c(1, 2); - // - let abc = new String("abc"); - let abc = new Abc.Def("abc"); - let abc = new java.lang.String("abc"); - const map = new HashMap(); - // - let def = new String("def").trim(); - let def = 123, lmn = 456 + abc; - const c = a.b.c; - const def = {a, b: 1}; - const arr = [a, b, c]; - arr[0] = 'a'; - // - let xyz; - xyz = "234"; - // - const b = s instanceof string; - // - const fn1 = (a, b) => a + b; - function fn2(a, b) { - const c = 5; - return a + b + c; - } - function fn3(a: string, b: number, c: XJsonDomainHandler, d: XJsonDomainHandler.Sub) { - return a + b + c.getName() + d.getName(); - } - // - if (a > 2) { - let b = 3; - a.b(b, 1); - } - """, // - "/test/ast/statement-1.ast"); + assertASTTree(""" + import java.lang.String; + import java.lang.Number; + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + // + const abc = ormTemplate.findListByQuery(query, mapper); + // + a(1, 2); + a.b.c(1, 2); + // + let abc = new String("abc"); + let abc = new Abc.Def("abc"); + let abc = new java.lang.String("abc"); + const map = new HashMap(); + // + let def = new String("def").trim(); + let def = 123, lmn = 456 + abc; + const c = a.b.c; + const def = {a, b: 1}; + const arr = [a, b, c]; + arr[0] = 'a'; + // + let xyz; + xyz = "234"; + // + const b = s instanceof string; + // + const fn1 = (a, b) => a + b; + function fn2(a, b) { + const c = 5; + return a + b + c; + } + function fn3(a: string, b: number, c: XJsonDomainHandler, d: XJsonDomainHandler.Sub) { + return a + b + c.getName() + d.getName(); + } + // + if (a > 2) { + let b = 3; + a.b(b, 1); + } + """, // + "/test/ast/xlang-script-1.ast"); } - protected void assertParseTree(String code, String expectedAstFile) { + protected void assertASTTree(String code, String expectedAstFile) { PsiFile testFile = myFixture.configureByText("sample." + ext, code); - String testTree = toParseTreeText(testFile); - String expectedTree = readVfsResource(expectedAstFile); - - assertEquals(expectedTree, testTree); - } - - protected String toParseTreeText(@NotNull PsiElement tree) { - return DebugUtil.psiToString(tree, true, false); + assertASTTree(testFile, expectedAstFile); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast new file mode 100644 index 000000000..e69de29bb diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-1.ast similarity index 100% rename from nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-1.ast rename to nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-1.ast diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-err-1.ast similarity index 100% rename from nop-idea-plugin/src/test/resources/_vfs/test/ast/statement-err-1.ast rename to nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-err-1.ast -- Gitee From b09879cfc808a869a89fbb238d72fe0c7b2f6c8b Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 9 Jul 2025 17:40:04 +0800 Subject: [PATCH 46/82] =?UTF-8?q?nop-idea-pugin:=20=E4=B8=BA=20XLang=20?= =?UTF-8?q?=E7=9A=84=20PSI=20=E6=A0=91=E7=9A=84=E5=85=B3=E9=94=AE=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=AE=9A=E4=B9=89=E6=9C=89=E7=A1=AE=E5=AE=9A=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E5=90=AB=E4=B9=89=E7=9A=84=20class=EF=BC=8C=E6=96=B9?= =?UTF-8?q?=E4=BE=BF=E5=AF=B9=E5=85=B6=E7=BB=93=E6=9E=84=E5=92=8C=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E8=BF=9B=E8=A1=8C=E5=88=86=E6=9E=90=E5=92=8C=E8=AF=86?= =?UTF-8?q?=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/lang/XLangParserDefinition.java | 43 ++++++ .../lang/XLangScriptLanguageInjector.java | 2 + .../idea/plugin/lang/psi/XLangAttribute.java | 17 +++ .../plugin/lang/psi/XLangAttributeValue.java | 17 +++ .../idea/plugin/lang/psi/XLangDocument.java | 15 ++ .../io/nop/idea/plugin/lang/psi/XLangTag.java | 122 +++++++++++++++ .../nop/idea/plugin/lang/psi/XLangText.java | 21 +++ .../idea/plugin/lang/psi/XLangTextToken.java | 24 +++ .../idea/plugin/lang/psi/XLangValueToken.java | 23 +++ .../nop/idea/plugin/utils/XDefPsiHelper.java | 1 + .../src/main/resources/META-INF/plugin.xml | 24 +-- .../nop/idea/plugin/lang/TestXLangParser.java | 21 ++- .../test/resources/_vfs/test/ast/xlang-1.ast | 139 ++++++++++++++++++ 13 files changed, 456 insertions(+), 13 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangDocument.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java index a86cfa498..a207a5f05 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java @@ -18,12 +18,28 @@ import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.source.tree.CompositeElement; +import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.xml.XmlFileImpl; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.IFileElementType; import com.intellij.psi.tree.TokenSet; +import io.nop.idea.plugin.lang.psi.XLangAttribute; +import io.nop.idea.plugin.lang.psi.XLangAttributeValue; +import io.nop.idea.plugin.lang.psi.XLangDocument; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.lang.psi.XLangText; +import io.nop.idea.plugin.lang.psi.XLangTextToken; +import io.nop.idea.plugin.lang.psi.XLangValueToken; import org.jetbrains.annotations.NotNull; +import static com.intellij.psi.xml.XmlElementType.XML_ATTRIBUTE; +import static com.intellij.psi.xml.XmlElementType.XML_ATTRIBUTE_VALUE; +import static com.intellij.psi.xml.XmlElementType.XML_DOCUMENT; +import static com.intellij.psi.xml.XmlElementType.XML_TAG; +import static com.intellij.psi.xml.XmlElementType.XML_TEXT; +import static com.intellij.psi.xml.XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN; +import static com.intellij.psi.xml.XmlTokenType.XML_DATA_CHARACTERS; + /** * 复用 XML 的解析能力,并对得到的 {@link PsiElement} 做 XLang 节点包装 *

@@ -51,9 +67,36 @@ public class XLangParserDefinition extends XmlASTFactory implements ParserDefini @Override public CompositeElement createComposite(@NotNull IElementType type) { + if (type == XML_DOCUMENT) { + return new XLangDocument(); + } // + else if (type == XML_TAG) { + return new XLangTag(); + } // + else if (type == XML_ATTRIBUTE) { + return new XLangAttribute(); + } // + else if (type == XML_ATTRIBUTE_VALUE) { + return new XLangAttributeValue(); + } // + else if (type == XML_TEXT) { + return new XLangText(); + } return super.createComposite(type); } + @Override + public LeafElement createLeaf(@NotNull final IElementType type, @NotNull CharSequence text) { + if (type == XML_DATA_CHARACTERS) { + return new XLangTextToken(type, text); + } // + else if (type == XML_ATTRIBUTE_VALUE_TOKEN) { + return new XLangValueToken(type, text); + } // + + return super.createLeaf(type, text); + } + // <<<<<<<<<<<<<<<<<< 委派到 XMLParserDefinition,从而复用 xml 的 AST 解析逻辑 private final XMLParserDefinition xmlParserDefinition = new XMLParserDefinition(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java index c35df368d..78f6a088c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java @@ -53,6 +53,8 @@ public class XLangScriptLanguageInjector implements LanguageInjector { }; if (lang != null) { + // Note: 第二个参数为有效文本在 host 中的范围,而 XmlText 的文本是不包含 CDATA 标签的, + // 因此,直接根据文本内容确定有效文本的范围即可 registrar.addPlace(lang, TextRange.create(0, host.getTextLength()), null, null); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java new file mode 100644 index 000000000..6be056302 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.lang.psi; + +import com.intellij.psi.impl.source.xml.XmlAttributeImpl; + +/** + * 属性,由名字(含名字空间)、等号和 {@link XLangAttributeValue} 组成 + * + * @author flytreeleft + * @date 2025-07-09 + */ +public class XLangAttribute extends XmlAttributeImpl { + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + getElementType() + "('" + getName() + "')"; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java new file mode 100644 index 000000000..d0271b28a --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -0,0 +1,17 @@ +package io.nop.idea.plugin.lang.psi; + +import com.intellij.psi.impl.source.xml.XmlAttributeValueImpl; + +/** + * 属性值,由引号和 {@link XLangValueToken} 组成 + * + * @author flytreeleft + * @date 2025-07-09 + */ +public class XLangAttributeValue extends XmlAttributeValueImpl { + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangDocument.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangDocument.java new file mode 100644 index 000000000..aaffd555f --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangDocument.java @@ -0,0 +1,15 @@ +package io.nop.idea.plugin.lang.psi; + +import com.intellij.psi.impl.source.xml.XmlDocumentImpl; + +/** + * @author flytreeleft + * @date 2025-07-09 + */ +public class XLangDocument extends XmlDocumentImpl { + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java new file mode 100644 index 000000000..890af0592 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -0,0 +1,122 @@ +package io.nop.idea.plugin.lang.psi; + +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceService; +import com.intellij.psi.impl.source.xml.XmlTagImpl; +import io.nop.core.lang.xml.XNode; +import io.nop.idea.plugin.utils.XDefPsiHelper; +import io.nop.xlang.xdef.IXDefAttribute; +import io.nop.xlang.xdef.IXDefNode; +import io.nop.xlang.xdef.IXDefinition; +import org.jetbrains.annotations.NotNull; + +/** + * {@link XNode} 标签(其名字含名字空间) + *

+ * 负责识别标签、属性、属性值的引用 + * + * @author flytreeleft + * @date 2025-07-09 + */ +public class XLangTag extends XmlTagImpl { + private XDefMeta defMeta; + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + getElementType() + "('" + getName() + "')"; + } + + @Override + public void clearCaches() { + this.defMeta = null; + + super.clearCaches(); + } + + @Override + public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { + // TODO 通过 CachedValuesManager.getCachedValue 缓存结果,并支持在依赖项变更时丢弃缓存结果 +// if (hints == PsiReferenceService.Hints.NO_HINTS) { +// return CachedValuesManager.getCachedValue(this, +// () -> CachedValueProvider.Result.create(getReferencesImpl( +// PsiReferenceService.Hints.NO_HINTS), +// PsiModificationTracker.MODIFICATION_COUNT, +// externalResourceModificationTracker( +// myTag))).clone(); +// } + + // TODO 合并 xml 与 xlang 的引用 + return super.getReferences(hints); + } + + /** 获取当前标签上指定属性的 xdef 定义 */ + public IXDefAttribute getDefAttr(String attrName) { + return null; + } + + public IXDefinition getDef() { + prepareDef(); + return defMeta.def; + } + + public IXDefNode getDefNode() { + prepareDef(); + return defMeta.defNode; + } + + public IXDefNode getXDslDefNode() { + prepareDef(); + return defMeta.xdslDefNode; + } + + private void prepareDef() { + if (defMeta != null) { + return; + } + + XLangTag parentTag = (XLangTag) getParentTag(); + // 当前为根节点 + if (parentTag == null) { + String schemaUrl = XDefPsiHelper.getSchemaPath(this); + if (schemaUrl == null) { + return; + } + + IXDefinition def = XDefPsiHelper.loadSchema(schemaUrl); + if (def == null) { + return; + } + + IXDefNode defNode = def.getRootNode(); + IXDefNode xdslDefNode = XDefPsiHelper.getXDslDef().getRootNode(); + + defMeta = new XDefMeta(def, defNode, xdslDefNode); + return; + } + + IXDefinition def = parentTag.getDef(); + if (def == null) { + return; + } + + String tagName = getName(); + IXDefNode parentDefNode = parentTag.getDefNode(); + IXDefNode parentXDslDefNode = parentTag.getXDslDefNode(); + + IXDefNode defNode = parentDefNode != null ? parentDefNode.getChild(tagName) : null; + IXDefNode xdslDefNode = parentXDslDefNode != null ? parentXDslDefNode.getChild(tagName) : null; + + defMeta = new XDefMeta(def, defNode, xdslDefNode); + } + + /** + * @param def + * 当前标签的元模型(在 *.xdef 中定义) + * @param defNode + * 当前标签在 {@link #def} 中所对应的节点 + * @param xdslDefNode + * 当前标签所对应的 xdsl.xdef 中的节点, + * 所有 DSL 模型的节点均与 xdsl.xdef 的节点存在对应 + */ + private record XDefMeta(IXDefinition def, IXDefNode defNode, IXDefNode xdslDefNode) {} +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java new file mode 100644 index 000000000..dea56066f --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java @@ -0,0 +1,21 @@ +package io.nop.idea.plugin.lang.psi; + +import com.intellij.psi.impl.source.xml.XmlTextImpl; + +/** + * 节点中的文本节点 + *

+ * 包含 CDATA 节点({@link com.intellij.psi.xml.XmlElementType#XML_CDATA XML_CDATA}), + * 并且,除了 CDATA 的文本是一个整体外,其余的文本会被拆分为空白和非空白两类 Token 作为 + * {@link XLangText} 的直接叶子节点 + * + * @author flytreeleft + * @date 2025-07-09 + */ +public class XLangText extends XmlTextImpl { + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java new file mode 100644 index 000000000..865c5de2f --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java @@ -0,0 +1,24 @@ +package io.nop.idea.plugin.lang.psi; + +import com.intellij.psi.impl.source.xml.XmlTokenImpl; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * CDATA 节点({@link com.intellij.psi.xml.XmlElementType#XML_CDATA XML_CDATA})中的全部文本, + * 或者 {@link XLangText} 节点中的非空白文本 + * + * @author flytreeleft + * @date 2025-07-09 + */ +public class XLangTextToken extends XmlTokenImpl { + + public XLangTextToken(@NotNull IElementType type, CharSequence text) { + super(type, text); + } + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + getTokenType(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java new file mode 100644 index 000000000..86f55003b --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java @@ -0,0 +1,23 @@ +package io.nop.idea.plugin.lang.psi; + +import com.intellij.psi.impl.source.xml.XmlTokenImpl; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * {@link XLangAttributeValue} 中不含引号的部分 + * + * @author flytreeleft + * @date 2025-07-09 + */ +public class XLangValueToken extends XmlTokenImpl { + + public XLangValueToken(@NotNull IElementType type, CharSequence text) { + super(type, text); + } + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + getTokenType(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 2b8fcaaf5..5e9402290 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -81,6 +81,7 @@ public class XDefPsiHelper { return ns; } + /** 从根节点获取 dsl 的元模型的 vfs 路径 */ public static String getSchemaPath(XmlTag rootTag) { PsiFile file = rootTag.getContainingFile(); String fileExt = StringHelper.fileExt(file.getName()); diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index e81408f6b..082d84079 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -74,26 +74,30 @@ + - - - + - + - + + - - - + - + + /nop/schema/xdef.xdef,/nop/schema/xdsl.xdef + + /nop/schema/xdef.xdef, + /nop/schema/xdsl.xdef + + + + This is a text. + + This is a child tag. + + + """, // "/test/ast/xlang-1.ast"); diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast index e69de29bb..30f8cf35f 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast @@ -0,0 +1,139 @@ +XmlFile:unit.xtest + XLangDocument + PsiElement(XML_PROLOG) + + XLangTag:XML_TAG('example') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('example') + PsiWhiteSpace(' ') + XLangAttribute:XML_ATTRIBUTE('xmlns:x') + XmlToken:XML_NAME('xmlns:x') + XmlToken:XML_EQ('=') + XLangAttributeValue + XmlToken:XML_ATTRIBUTE_VALUE_START_DELIMITER('"') + XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN('/nop/schema/xdsl.xdef') + XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"') + PsiWhiteSpace('\n ') + XLangAttribute:XML_ATTRIBUTE('x:schema') + XmlToken:XML_NAME('x:schema') + XmlToken:XML_EQ('=') + XLangAttributeValue + XmlToken:XML_ATTRIBUTE_VALUE_START_DELIMITER('"') + XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN('/path/to/example.xdef') + XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"') + PsiWhiteSpace('\n') + XmlToken:XML_TAG_END('>') + XLangText + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('tag') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('tag') + PsiWhiteSpace(' ') + XLangAttribute:XML_ATTRIBUTE('name') + XmlToken:XML_NAME('name') + XmlToken:XML_EQ('=') + XLangAttributeValue + XmlToken:XML_ATTRIBUTE_VALUE_START_DELIMITER('"') + XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN('Child') + XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"') + XmlToken:XML_EMPTY_ELEMENT_END('/>') + XLangText + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('refs') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('refs') + XmlToken:XML_TAG_END('>') + XLangText + XLangTextToken:XML_DATA_CHARACTERS('/nop/schema/xdef.xdef,/nop/schema/xdsl.xdef') + XmlToken:XML_END_TAG_START('') + XLangText + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('refs') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('refs') + XmlToken:XML_TAG_END('>') + XLangText + PsiWhiteSpace('\n ') + XLangTextToken:XML_DATA_CHARACTERS('/nop/schema/xdef.xdef,') + PsiWhiteSpace('\n ') + XLangTextToken:XML_DATA_CHARACTERS('/nop/schema/xdsl.xdef') + PsiWhiteSpace('\n ') + XmlToken:XML_END_TAG_START('') + XLangText + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('text') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('text') + XmlToken:XML_TAG_END('>') + XLangText + PsiElement(XML_CDATA) + XmlToken:XML_CDATA_START('') + XmlToken:XML_END_TAG_START('') + XLangText + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('mix') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('mix') + XmlToken:XML_TAG_END('>') + XLangText + PsiWhiteSpace('\n ') + XLangTextToken:XML_DATA_CHARACTERS('This') + PsiWhiteSpace(' ') + XLangTextToken:XML_DATA_CHARACTERS('is') + PsiWhiteSpace(' ') + XLangTextToken:XML_DATA_CHARACTERS('a') + PsiWhiteSpace(' ') + XLangTextToken:XML_DATA_CHARACTERS('text.') + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('tag') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('tag') + XmlToken:XML_TAG_END('>') + XLangText + PsiWhiteSpace('\n ') + XLangTextToken:XML_DATA_CHARACTERS('This') + PsiWhiteSpace(' ') + XLangTextToken:XML_DATA_CHARACTERS('is') + PsiWhiteSpace(' ') + XLangTextToken:XML_DATA_CHARACTERS('a') + PsiWhiteSpace(' ') + XLangTextToken:XML_DATA_CHARACTERS('child') + PsiWhiteSpace(' ') + XLangTextToken:XML_DATA_CHARACTERS('tag.') + PsiWhiteSpace('\n ') + XmlToken:XML_END_TAG_START('') + XLangText + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('tag') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('tag') + XmlToken:XML_TAG_END('>') + XLangText + PsiElement(XML_CDATA) + XmlToken:XML_CDATA_START('') + XmlToken:XML_END_TAG_START('') + XLangText + PsiWhiteSpace('\n ') + XmlToken:XML_END_TAG_START('') + XLangText + PsiWhiteSpace('\n') + XmlToken:XML_END_TAG_START('') + PsiWhiteSpace('\n') -- Gitee From 558537ad973cf4968430c42b0c531cebc18f834b Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 10 Jul 2025 21:39:21 +0800 Subject: [PATCH 47/82] =?UTF-8?q?nop-idea-pugin:=20=E5=88=9D=E6=AD=A5?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc/AntDomDocumentationProvider.java | 2 +- .../idea/plugin/lang/psi/XLangAttribute.java | 61 +++ .../io/nop/idea/plugin/lang/psi/XLangTag.java | 184 +++++-- .../reference/XLangAttributeReference.java | 48 ++ .../nop/idea/plugin/utils/XDefPsiHelper.java | 15 + .../completion/TestAutoPopupCompletion.java | 1 - .../TestXLangCompletionContributor.java | 1 - .../idea/plugin/lang/TestXLangReferences.java | 454 ++++++++++++++++++ .../reference/TestXLangReferenceProvider.java | 446 ----------------- 9 files changed, 726 insertions(+), 486 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java delete mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/AntDomDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/AntDomDocumentationProvider.java index 5b18653e2..d21d2616f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/AntDomDocumentationProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/AntDomDocumentationProvider.java @@ -178,7 +178,7 @@ // // @Override // @Nullable -// public @Nls String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { // todo! +// public @Nls String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { // if (element instanceof PomTargetPsiElement) { // final PomTarget pomTarget = ((PomTargetPsiElement)element).getTarget(); // if (pomTarget instanceof DomTarget) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java index 6be056302..d67983d4f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -1,6 +1,13 @@ package io.nop.idea.plugin.lang.psi; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.XmlAttributeImpl; +import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.reference.XLangAttributeReference; +import io.nop.xlang.xdef.IXDefAttribute; +import org.jetbrains.annotations.NotNull; /** * 属性,由名字(含名字空间)、等号和 {@link XLangAttributeValue} 组成 @@ -14,4 +21,58 @@ public class XLangAttribute extends XmlAttributeImpl { public String toString() { return getClass().getSimpleName() + ':' + getElementType() + "('" + getName() + "')"; } + + @Override + public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { + XmlElement attrName = getNameElement(); + if (attrName == null) { + return PsiReference.EMPTY_ARRAY; + } + + TextRange textRange = attrName.getTextRangeInParent(); + XLangAttributeReference ref = new XLangAttributeReference(this, textRange); + + //return super.getReferences(hints); + return new PsiReference[] { ref }; + } + + /** 获取当前属性的 xdef 定义 */ + public IXDefAttribute getXDefAttr() { + XLangTag tag = (XLangTag) getParent(); + if (tag == null) { + return null; + } + + // - 对于声明属性,从其自身(*.xdef)中取其定义 + // - 对于赋值属性,从其 x:schema 中取其定义 + // - 对于名字空间对应 xdsl.xdef 和 xdef.xdef 的属性,则分别从这两个元模型中取属性定义 + IXDefAttribute attrDef; + + String attrName = getName(); + String xdefNs = tag.getXDefNs(); + String xdslNs = tag.getXDslNs(); + + if (tag.isInXDef()) { + // 取 xdef.xdef 中声明的属性 + if (attrName.startsWith(xdefNs + ':')) { + attrDef = tag.getXDefNodeAttr(attrName); + } + // 取 xdsl.xdef 中声明的属性 + else if (attrName.startsWith(xdslNs + ':')) { + attrDef = tag.getXDslDefNodeAttr(attrName); + } + // 取自身声明的属性 + else { + attrDef = tag.getSelfDefNodeAttr(attrName); + } + } else { + if (attrName.startsWith(xdslNs + ':')) { + attrDef = tag.getXDslDefNodeAttr(attrName); + } else { + attrDef = tag.getXDefNodeAttr(attrName); + } + } + + return attrDef; + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 890af0592..ccce9fc72 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -1,13 +1,19 @@ package io.nop.idea.plugin.lang.psi; +import com.intellij.openapi.project.Project; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.XmlTagImpl; import io.nop.core.lang.xml.XNode; +import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; +import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefinition; +import io.nop.xlang.xdef.XDefKeys; +import io.nop.xlang.xdsl.XDslConstants; +import io.nop.xlang.xdsl.XDslKeys; import org.jetbrains.annotations.NotNull; /** @@ -19,7 +25,7 @@ import org.jetbrains.annotations.NotNull; * @date 2025-07-09 */ public class XLangTag extends XmlTagImpl { - private XDefMeta defMeta; + private SchemaMeta schemaMeta; @Override public String toString() { @@ -28,7 +34,7 @@ public class XLangTag extends XmlTagImpl { @Override public void clearCaches() { - this.defMeta = null; + this.schemaMeta = null; super.clearCaches(); } @@ -49,74 +55,178 @@ public class XLangTag extends XmlTagImpl { return super.getReferences(hints); } - /** 获取当前标签上指定属性的 xdef 定义 */ - public IXDefAttribute getDefAttr(String attrName) { - return null; + /** 当前标签是否在 xdef 文件中 */ + public boolean isInXDef() { + return getSelfDefNode() != null; } - public IXDefinition getDef() { - prepareDef(); - return defMeta.def; + /** @see SchemaMeta#xdef */ + public IXDefinition getXDef() { + prepareSchema(); + return schemaMeta.xdef; } - public IXDefNode getDefNode() { - prepareDef(); - return defMeta.defNode; + /** @see SchemaMeta#xdefNode */ + public IXDefNode getXDefNode() { + prepareSchema(); + return schemaMeta.xdefNode; } + public IXDefAttribute getXDefNodeAttr(String attrName) { + // Note: xdef.xdef 的属性在固定的名字空间 xdef 中声明 + attrName = changeNamespace(attrName, getXDefNs(), XDefKeys.DEFAULT.NS); + + return getXDefNodeAttr(getXDefNode(), attrName); + } + + /** @see SchemaMeta#xdslDefNode */ public IXDefNode getXDslDefNode() { - prepareDef(); - return defMeta.xdslDefNode; + prepareSchema(); + return schemaMeta.xdslDefNode; + } + + public IXDefAttribute getXDslDefNodeAttr(String attrName) { + // Note: xdsl.xdef 的属性在固定的名字空间 x 中声明 + String xNs = XDslKeys.DEFAULT.X_NS_PREFIX.substring(0, XDslKeys.DEFAULT.X_NS_PREFIX.length() - 1); + attrName = changeNamespace(attrName, getXDslNs(), xNs); + + return getXDefNodeAttr(getXDslDefNode(), attrName); + } + + /** @see SchemaMeta#selfDefNode */ + public IXDefNode getSelfDefNode() { + prepareSchema(); + return schemaMeta.selfDefNode; + } + + public IXDefAttribute getSelfDefNodeAttr(String attrName) { + return getXDefNodeAttr(getSelfDefNode(), attrName); + } + + /** @see SchemaMeta#xdefNs */ + public String getXDefNs() { + prepareSchema(); + return schemaMeta.xdefNs; + } + + /** @see SchemaMeta#xdslNs */ + public String getXDslNs() { + prepareSchema(); + return schemaMeta.xdslNs; + } + + /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ + private static IXDefAttribute getXDefNodeAttr(IXDefNode xdefNode, String attrName) { + IXDefAttribute attr = xdefNode != null ? xdefNode.getAttribute(attrName) : null; + + return attr; + } + + private static String changeNamespace(String name, String fromNs, String toNs) { + if (fromNs.equals(toNs)) { + return name; + } + + String ns = fromNs + ':'; + if (name.startsWith(ns)) { + return toNs + ':' + name.substring(ns.length()); + } + return name; } - private void prepareDef() { - if (defMeta != null) { + private synchronized void prepareSchema() { + if (this.schemaMeta != null) { return; } + Project project = getProject(); + schemaMeta = ProjectEnv.withProject(project, this::createSchemaMeta); + } + + private SchemaMeta createSchemaMeta() { XLangTag parentTag = (XLangTag) getParentTag(); - // 当前为根节点 + + // 当前为根标签 if (parentTag == null) { String schemaUrl = XDefPsiHelper.getSchemaPath(this); if (schemaUrl == null) { - return; + return SchemaMeta.UNDEFINED; + } + + IXDefinition xdef = XDefPsiHelper.loadSchema(schemaUrl); + if (xdef == null) { + return SchemaMeta.UNDEFINED; } - IXDefinition def = XDefPsiHelper.loadSchema(schemaUrl); - if (def == null) { - return; + String xdefNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDEF); + String xdslNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDSL); + + IXDefNode selfDefNode = null; + // x:schema 为 /nop/schema/xdef.xdef 的,均为 xdef 模型 + if (XDslConstants.XDSL_SCHEMA_XDEF.equals(getAttributeValue(xdslNs + ":schema"))) { + String vfsPath = XmlPsiHelper.getNopVfsPath(this); + + IXDefinition selfDef; + if (vfsPath != null) { + selfDef = XDefPsiHelper.loadSchema(vfsPath); + } else { + // 适配单元测试环境:待测试资源可能不是标准的 vfs 资源 + selfDef = XDefPsiHelper.loadSchema(getContainingFile()); + } + + selfDefNode = selfDef != null ? selfDef.getRootNode() : null; } - IXDefNode defNode = def.getRootNode(); + IXDefNode xdefNode = xdef.getRootNode(); IXDefNode xdslDefNode = XDefPsiHelper.getXDslDef().getRootNode(); - defMeta = new XDefMeta(def, defNode, xdslDefNode); - return; + return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefNs, xdslNs); } - IXDefinition def = parentTag.getDef(); - if (def == null) { - return; + IXDefinition xdef = parentTag.getXDef(); + if (xdef == null) { + return SchemaMeta.UNDEFINED; } - String tagName = getName(); - IXDefNode parentDefNode = parentTag.getDefNode(); + String xdefNs = parentTag.getXDefNs(); + String xdslNs = parentTag.getXDslNs(); + IXDefNode parentXDefNode = parentTag.getXDefNode(); IXDefNode parentXDslDefNode = parentTag.getXDslDefNode(); + IXDefNode parentSelfDefNode = parentTag.getSelfDefNode(); + + String tagName = getName(); + tagName = changeNamespace(tagName, xdefNs, XDefKeys.DEFAULT.NS); - IXDefNode defNode = parentDefNode != null ? parentDefNode.getChild(tagName) : null; + IXDefNode xdefNode = parentXDefNode != null ? parentXDefNode.getChild(tagName) : null; IXDefNode xdslDefNode = parentXDslDefNode != null ? parentXDslDefNode.getChild(tagName) : null; + IXDefNode selfDefNode = parentSelfDefNode != null ? parentSelfDefNode.getChild(tagName) : null; - defMeta = new XDefMeta(def, defNode, xdslDefNode); + return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefNs, xdslNs); } /** - * @param def - * 当前标签的元模型(在 *.xdef 中定义) - * @param defNode - * 当前标签在 {@link #def} 中所对应的节点 + * @param xdef + * 当前标签所在的元模型(在 *.xdef 中定义) + * @param xdefNode + * 当前标签在 {@link #xdef} 中所对应的节点 * @param xdslDefNode - * 当前标签所对应的 xdsl.xdef 中的节点, - * 所有 DSL 模型的节点均与 xdsl.xdef 的节点存在对应 + * 当前标签在 xdsl.xdef 中所对应的节点。 + * 注:所有 DSL 模型的节点均与 xdsl.xdef 的节点存在对应 + * @param selfDefNode + * 若当前标签定义在 xdef 文件中,则需得到其自身的定义节点 + * @param xdefNs + * /nop/schema/xdef.xdef 对应的名字空间。 + * 仅在元模型中设置,如 xmlns:xdef="/nop/schema/xdef.xdef" + * @param xdslNs + * /nop/schema/xdsl.xdef 对应的名字空间。 + * 在 DSL 模型(含元模型)中均有设置,如 xmlns:x="/nop/schema/xdsl.xdef" */ - private record XDefMeta(IXDefinition def, IXDefNode defNode, IXDefNode xdslDefNode) {} + private record SchemaMeta( // + IXDefinition xdef, IXDefNode xdefNode, // + IXDefNode xdslDefNode, // + IXDefNode selfDefNode, // + String xdefNs, String xdslNs // + ) { + public static final SchemaMeta UNDEFINED = new SchemaMeta(null, null, null, null, null, null); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java new file mode 100644 index 000000000..392882c89 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java @@ -0,0 +1,48 @@ +package io.nop.idea.plugin.lang.reference; + +import java.util.Objects; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlAttribute; +import io.nop.api.core.util.SourceLocation; +import io.nop.idea.plugin.lang.psi.XLangAttribute; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.xlang.xdef.IXDefAttribute; +import org.jetbrains.annotations.Nullable; + +/** + * @author flytreeleft + * @date 2025-07-10 + */ +public class XLangAttributeReference extends XLangReferenceBase { + + public XLangAttributeReference(XLangAttribute myElement, TextRange myRangeInElement) { + super(myElement, myRangeInElement); + } + + @Override + public @Nullable PsiElement resolveInner() { + XLangAttribute attr = (XLangAttribute) myElement; + IXDefAttribute attrDef = attr.getXDefAttr(); + if (attrDef == null) { + return null; + } + + XLangTag tag = (XLangTag) attr.getParent(); + SourceLocation loc = attrDef.getLocation(); + // Note: SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 + String path = loc != null // + ? loc.getPath().replace("classpath:_vfs", "") // + : tag.getXDef().resourcePath(); + + PsiElement[] targets = XmlPsiHelper.findPsiFilesByNopVfsPath(attr, path) + .stream() + .map((file) -> XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class)) + .filter(Objects::nonNull) + .toArray(PsiElement[]::new); + + return targets[0]; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 5e9402290..5f6c6374e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -15,7 +15,9 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.xml.XmlTag; import io.nop.commons.util.CollectionHelper; import io.nop.commons.util.StringHelper; +import io.nop.core.resource.IResource; import io.nop.core.resource.impl.ClassPathResource; +import io.nop.core.resource.impl.InMemoryTextResource; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefinition; import io.nop.xlang.xdef.XDefKeys; @@ -106,6 +108,19 @@ public class XDefPsiHelper { } } + public static IXDefinition loadSchema(PsiFile file) { + String content = file.getText(); + // Note: 解析过程中,会检查路径的有效性,需保证以 / 开头,并添加 .xdef 后缀 + IResource resource = new InMemoryTextResource('/' + file.getVirtualFile().getName() + ".xdef", content); + + try { + return new XDefinitionParser().parseFromResource(resource); + } catch (Exception e) { + LOG.debug("nop.load-schema-fail", e); + return null; + } + } + public static XmlTagInfo getTagInfo(PsiElement element) { XmlTag tag = getXmlTag(element); if (tag == null) { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestAutoPopupCompletion.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestAutoPopupCompletion.java index 8de3f4a1c..3f92ecf23 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestAutoPopupCompletion.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestAutoPopupCompletion.java @@ -26,7 +26,6 @@ public class TestAutoPopupCompletion extends BasePlatformTestCase { tester.joinCompletion(); // tester.getLookup().getItems(); - // fixme: test completion items via tester.getLookup() }); } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java index ca08c53a7..8631c45bb 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java @@ -32,6 +32,5 @@ public class TestXLangCompletionContributor extends LightPlatformCodeInsightFixt myFixture.type("foo"); LookupElement[] items = myFixture.completeBasic(); - // fixme: test the completion items } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java new file mode 100644 index 000000000..4aa28b657 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -0,0 +1,454 @@ +package io.nop.idea.plugin.lang; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiPlainText; +import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlTag; +import io.nop.idea.plugin.BaseXLangPluginTestCase; +import io.nop.idea.plugin.reference.XLangElementReference; +import io.nop.idea.plugin.reference.XLangReference; +import io.nop.idea.plugin.reference.XLangVfsFileReference; +import io.nop.idea.plugin.reference.XLangXDefReference; +import io.nop.idea.plugin.utils.XmlPsiHelper; + +/** + * @author flytreeleft + * @date 2025-06-22 + */ +public class TestXLangReferences extends BaseXLangPluginTestCase { + + public void testTagReferences() { + assertReference(""" + + + ByMdxQuery xpl:lib="/test/reference/a.xlib"/> + + + """, "/test/reference/a.xlib#DoFindByMdxQuery"); + } + + public void testAttributeReferences() { + // 名字空间不做引用 + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", + "meta:unique-attr=\"name\""), null); + + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", + "meta:unique-attr=\"name\""), + "meta:define#xdef:unique-attr=xml-name"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), + "xdef:prop#name=!xml-name"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("ame=\"!var-name\""), + "xdef:define#xdef:name=!var-name"); + + assertReference(readVfsResource("/test/doc/example.xdef").replace("me=\"string\""), + "meta:define#xdef:unknown-attr=def-type"); + + assertReference(""" + + pe="leaf"/> + + """, "child#type=dict:test/doc/child-type"); + assertReference(""" + + ge="22"/> + + """, "child#xdef:unknown-attr=any"); + assertReference(""" + + ge="23"/> + + """, "xdef:unknown-tag#xdef:unknown-attr=any"); + + assertReference(""" + f="/test/reference/test-filter.xdef#FilterCondition" + /> + """, "schema#ref=xdef-ref"); + } + + public void testAttributeValueReferences() { + // 对 v-path 属性值的引用 + // - x:schema=v-path + assertReference(""" + + """, "/nop/schema/xmeta.xdef"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", + "x:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", + "xdsl:schema=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + // - xdef:default-extends=v-path + assertReference(""" + + """, "/test/reference/default.xform"); + // - xpl:lib=v-path + assertReference(""" + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); + // 对 v-path-list 列表元素的引用 + assertReference(""" + + """, "/test/reference/a.xmeta"); + assertReference(""" + + """, "/test/reference/b.xmeta"); + + // 对 xdef-ref 类型属性的引用 + // - 在 *.xdef 中引用内部名字 + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", + "meta:ref=\"XDefNode\""), + "meta:define#meta:name=XDefNode"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", + "xdef:ref=\"DslNode\""), + "xdef:unknown-tag#xdef:name=DslNode"); +// // - 引用文件的相对路径出现在开头 +// doTest(readVfsResource("/nop/schema/xui/simple-component.xdef").replace("xdef:ref=\"../xui/import.xdef\"", +// "xdef:ref=\"../xui/import.xdef\""), +// "/nop/schema/xui/import.xdef"); + assertReference(""" + + + + + """, "xdef:define#xdef:name=PropNode"); + assertReference(""" + + + + + """, null); + // - 在 *.xdef 中引用外部文件 + assertReference(""" + + """, "/nop/schema/xdsl.xdef"); + assertReference(""" + + """, "/nop/schema/schema/obj-schema.xdef"); + // - 在 *.xmeta 中引用外部文件中的节点 + assertReference(""" + + """, "xdef:define#xdef:name=FilterCondition"); + // - 外部文件中的引用节点不存在 + assertReference(""" + + """, null); + + // 对 x:prototype 属性值的引用 + assertReference(readVfsResource("/test/reference/user.view.xml").replace("x:prototype=\"list\"", + "x:prototype=\"list\""), + "grid#id=list"); + assertReference(readVfsResource("/test/reference/a.xlib").replace("x:prototype=\"Get\"", + "x:prototype=\"Get\""), "Get"); + // - 引用不存在 + assertReference(""" + + + + + + """, null); + assertReference(""" + + + + + + """, null); + + // 对唯一键的引用 + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", + "meta:unique-attr=\"name\""), + "xdef:prop#name=!xml-name"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"xdef:name\"", + "meta:unique-attr=\"xdef:name\""), + "xdef:define#xdef:name=!var-name"); + assertReference(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"id\"", + "xdef:key-attr=\"id\""), + "selection#id=!var-name"); + // - 引用不存在 + assertReference(""" + + + + """, null); + assertReference(""" + + + + + + """, null); + + assertReference(""" + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); + assertReference(""" + + + + + + + + + + """, "/nop/core/xlib/meta-gen.xlib"); + + // 非有效路径或未定义属性引用 + assertReference(readVfsResource("/test/reference/user.view.xml").replace("xmlns:view-gen=\"view-gen\"", + "xmlns:view-gen=\"view-gen\""), + null); + assertReference(""" +

+ """, null); + + // 未知 schema 导致引用无法识别,但支持对 *.xdef 的引用识别 + assertReference(""" + + """, null); + assertReference(""" + + """, "/nop/schema/xdsl.xdef"); + +// // TODO 对 xpl 属性的文件引用 +// doTest(""" +// +// """, "/test/reference/a.xlib"); +// doTest(""" +// +// """, "/test/reference/a.xlib"); + } + + public void testAttributeTypeReferences() { + // 声明属性将 引用 属性的类型定义 + // TODO 暂时无法通过分析 class 字节码得到可注册的数据域 +// // - #getName 返回引用值 +// doTest(""" +// +// +// +// """, "/dict/test/doc/child-type.dict.yaml#leaf"); +// // - #getName 返回字面量值 +// doTest(""" +// +// +// +// """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + // - 引用字典中定义的数据域 + assertReference(""" + + + + """, "/dict/core/std-domain.dict.yaml#string"); + + // 字典/枚举的 options 引用 + assertReference(""" + + + + """, "/dict/test/doc/child-type.dict.yaml"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace( + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + "io.nop.xlang.xdef.XDefOverride"); + + // 字典/枚举的默认值引用 + assertReference(""" + + + + """, "/dict/test/doc/child-type.dict.yaml#leaf"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace( + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + "io.nop.xlang.xdef.XDefOverride#MERGE"); + + // 缺省属性值中 @attr: 引用 + assertReference(""" + + + + """, "import#name=var-name"); + assertReference(""" + + + + """, "var#type=!string"); + assertReference(""" + + + + """, null); + } + + public void testTextReferences() { + assertReference(readVfsResource("/test/reference/user.view.xml").replace("", ""), + "/test/reference/a.xmeta"); + + assertReference(""" + + /test/reference/test-filter.xdef,/nop/schema/xdsl.xdef + + """, "/test/reference/test-filter.xdef"); + assertReference(""" + + + /test/reference/test-filter.xdef,/nop/schema/xdsl.xdef + + + """, "/nop/schema/xdsl.xdef"); + assertReference(""" + + t-filter.xdef, /nop/schema/xdsl.xdef + ]]> + + """, "/test/reference/test-filter.xdef"); + assertReference(""" + + /xdsl.xdef + ]]> + + """, "/nop/schema/xdsl.xdef"); + } + + /** 通过在 text 中插入 <caret> 代表光标位置 */ + private void assertReference(String text, String expected) { + configureByXLangText(text); + + PsiReference ref = findReferenceAtCaret(); + +// if (!(ref instanceof XLangReference) && expected == null) { +// return; // 不检查非 XLang 引用 +// } +// assertInstanceOf(ref, XLangReference.class); + + PsiElement target = ref.resolve(); + if (expected == null) { + assertNull(target); + return; + } + assertNotNull(target); + + if (ref instanceof XLangVfsFileReference) { + // Note: 可能不是 vfs 文件 + String vfsPath = XmlPsiHelper.getNopVfsPath(target); + String anchor = target instanceof XmlAttribute attr ? attr.getValue() : null; + + assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); + } // + else if (ref instanceof XLangElementReference || ref instanceof XLangXDefReference) { + if (target instanceof XmlTag tag) { + assertEquals(expected, tag.getName()); + } // + else if (target instanceof XmlAttribute attr) { + XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); + + assertEquals(expected, tag.getName() + "#" + attr.getName() + "=" + attr.getValue()); + } // + else if (target instanceof PsiClass cls) { + assertEquals(expected, cls.getQualifiedName()); + } // + else if (target instanceof PsiField field) { + assertEquals(expected, field.getContainingClass().getQualifiedName() + "#" + field.getName()); + } // + else if (target instanceof PsiPlainText txt) { + String vfsPath = XmlPsiHelper.getNopVfsPath(target); + + assertEquals(expected, vfsPath + ":" + txt.getTextOffset()); + } // + else if (target instanceof LeafPsiElement leaf) { + String vfsPath = XmlPsiHelper.getNopVfsPath(target); + + assertEquals(expected, vfsPath + "#" + leaf.getText()); + } // + else { + fail("Unknown target " + target.getClass()); + } + } + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java deleted file mode 100644 index cc47d5c59..000000000 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/reference/TestXLangReferenceProvider.java +++ /dev/null @@ -1,446 +0,0 @@ -package io.nop.idea.plugin.reference; - -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiPlainText; -import com.intellij.psi.PsiReference; -import com.intellij.psi.impl.source.tree.LeafPsiElement; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlTag; -import io.nop.idea.plugin.BaseXLangPluginTestCase; -import io.nop.idea.plugin.utils.XmlPsiHelper; - -/** - * @author flytreeleft - * @date 2025-06-22 - */ -public class TestXLangReferenceProvider extends BaseXLangPluginTestCase { - - public void testGetReferencesFromXmlAttributeValue() { - // 对 v-path 属性值的引用 - // - x:schema=v-path - doTest(""" - - """, "/nop/schema/xmeta.xdef"); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", - "x:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", - "xdsl:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - // - xdef:default-extends=v-path - doTest(""" - - """, "/test/reference/default.xform"); - // - xpl:lib=v-path - doTest(""" - - - - - - """, "/nop/core/xlib/meta-gen.xlib"); - // 对 v-path-list 列表元素的引用 - doTest(""" - - """, "/test/reference/a.xmeta"); - doTest(""" - - """, "/test/reference/b.xmeta"); - - // 对 xdef-ref 类型属性的引用 - // - 在 *.xdef 中引用内部名字 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", - "meta:ref=\"XDefNode\""), - "meta:define#meta:name=XDefNode"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), - "xdef:unknown-tag#xdef:name=DslNode"); -// // - 引用文件的相对路径出现在开头 -// doTest(readVfsResource("/nop/schema/xui/simple-component.xdef").replace("xdef:ref=\"../xui/import.xdef\"", -// "xdef:ref=\"../xui/import.xdef\""), -// "/nop/schema/xui/import.xdef"); - doTest(""" - - - - - """, "xdef:define#xdef:name=PropNode"); - doTest(""" - - - - - """, null); - // - 在 *.xdef 中引用外部文件 - doTest(""" - - """, "/nop/schema/xdsl.xdef"); - doTest(""" - - """, "/nop/schema/schema/obj-schema.xdef"); - // - 在 *.xmeta 中引用外部文件中的节点 - doTest(""" - - """, "xdef:define#xdef:name=FilterCondition"); - // - 外部文件中的引用节点不存在 - doTest(""" - - """, null); - - // 对 x:prototype 属性值的引用 - doTest(readVfsResource("/test/reference/user.view.xml").replace("x:prototype=\"list\"", - "x:prototype=\"list\""), "grid#id=list"); - doTest(readVfsResource("/test/reference/a.xlib").replace("x:prototype=\"Get\"", "x:prototype=\"Get\""), - "Get"); - // - 引用不存在 - doTest(""" - - - - - - """, null); - doTest(""" - - - - - - """, null); - - // 对唯一键的引用 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), - "xdef:prop#name=!xml-name"); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"xdef:name\"", - "meta:unique-attr=\"xdef:name\""), - "xdef:define#xdef:name=!var-name"); - doTest(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"id\"", "xdef:key-attr=\"id\""), - "selection#id=!var-name"); - // - 引用不存在 - doTest(""" - - - - """, null); - doTest(""" - - - - - - """, null); - - doTest(""" - - - - - - """, "/nop/core/xlib/meta-gen.xlib"); - doTest(""" - - - - - - - - - - """, "/nop/core/xlib/meta-gen.xlib"); - - // 非有效路径或未定义属性引用 - doTest(readVfsResource("/test/reference/user.view.xml").replace("xmlns:view-gen=\"view-gen\"", - "xmlns:view-gen=\"view-gen\""), null); - doTest(""" - - """, null); - - // 未知 schema 导致引用无法识别,但支持对 *.xdef 的引用识别 - doTest(""" - - """, null); - doTest(""" - - """, "/nop/schema/xdsl.xdef"); - -// // TODO 对 xpl 属性的文件引用 -// doTest(""" -// -// """, "/test/reference/a.xlib"); -// doTest(""" -// -// """, "/test/reference/a.xlib"); - } - - public void testGetReferencesFromXmlAttributeType() { - // 声明属性将 引用 属性的类型定义 - // TODO 暂时无法通过分析 class 字节码得到可注册的数据域 -// // - #getName 返回引用值 -// doTest(""" -// -// -// -// """, "/dict/test/doc/child-type.dict.yaml#leaf"); -// // - #getName 返回字面量值 -// doTest(""" -// -// -// -// """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); - // - 引用字典中定义的数据域 - doTest(""" - - - - """, "/dict/core/std-domain.dict.yaml#string"); - - // 字典/枚举的 options 引用 - doTest(""" - - - - """, "/dict/test/doc/child-type.dict.yaml"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // - "io.nop.xlang.xdef.XDefOverride"); - - // 字典/枚举的默认值引用 - doTest(""" - - - - """, "/dict/test/doc/child-type.dict.yaml#leaf"); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace( - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // - "io.nop.xlang.xdef.XDefOverride#MERGE"); - - // 缺省属性值中 @attr: 引用 - doTest(""" - - - - """, "import#name=var-name"); - doTest(""" - - - - """, "var#type=!string"); - doTest(""" - - - - """, null); - } - - public void testGetReferencesFromXmlAttribute() { - // 名字空间不做引用 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), null); - - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), - "meta:define#xdef:unique-attr=xml-name"); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), - "xdef:prop#name=!xml-name"); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("ame=\"!var-name\""), - "xdef:define#xdef:name=!var-name"); - - doTest(readVfsResource("/test/doc/example.xdef").replace("me=\"string\""), - "meta:define#xdef:unknown-attr=def-type"); - - doTest(""" - - pe="leaf"/> - - """, "child#type=dict:test/doc/child-type"); - doTest(""" - - ge="22"/> - - """, "child#xdef:unknown-attr=any"); - doTest(""" - - ge="23"/> - - """, "xdef:unknown-tag#xdef:unknown-attr=any"); - - doTest(""" - f="/test/reference/test-filter.xdef#FilterCondition" - /> - """, "schema#ref=xdef-ref"); - } - - public void testGetReferencesFromXmlText() { - doTest(readVfsResource("/test/reference/user.view.xml").replace("", ""), - "/test/reference/a.xmeta"); - - doTest(""" - - /test/reference/test-filter.xdef,/nop/schema/xdsl.xdef - - """, "/test/reference/test-filter.xdef"); - doTest(""" - - - /test/reference/test-filter.xdef,/nop/schema/xdsl.xdef - - - """, "/nop/schema/xdsl.xdef"); - doTest(""" - - t-filter.xdef, /nop/schema/xdsl.xdef - ]]> - - """, "/test/reference/test-filter.xdef"); - doTest(""" - - /xdsl.xdef - ]]> - - """, "/nop/schema/xdsl.xdef"); - } - - public void testGetReferencesFromXmlTag() { - doTest(""" - - - ByMdxQuery xpl:lib="/test/reference/a.xlib"/> - - - """, "/test/reference/a.xlib#DoFindByMdxQuery"); - } - - /** 通过在 text 中插入 <caret> 代表光标位置 */ - private void doTest(String text, String expected) { - configureByXLangText(text); - - PsiReference ref = findReferenceAtCaret(); - - if (!(ref instanceof XLangReference) && expected == null) { - return; // 不检查非 XLang 引用 - } - assertInstanceOf(ref, XLangReference.class); - - PsiElement target = ref.resolve(); - if (expected == null) { - assertNull(target); - return; - } - assertNotNull(target); - - if (ref instanceof XLangVfsFileReference) { - // Note: 可能不是 vfs 文件 - String vfsPath = XmlPsiHelper.getNopVfsPath(target); - String anchor = target instanceof XmlAttribute attr ? attr.getValue() : null; - - assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); - } // - else if (ref instanceof XLangElementReference || ref instanceof XLangXDefReference) { - if (target instanceof XmlTag tag) { - assertEquals(expected, tag.getName()); - } // - else if (target instanceof XmlAttribute attr) { - XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); - - assertEquals(expected, tag.getName() + "#" + attr.getName() + "=" + attr.getValue()); - } // - else if (target instanceof PsiClass cls) { - assertEquals(expected, cls.getQualifiedName()); - } // - else if (target instanceof PsiField field) { - assertEquals(expected, field.getContainingClass().getQualifiedName() + "#" + field.getName()); - } // - else if (target instanceof PsiPlainText txt) { - String vfsPath = XmlPsiHelper.getNopVfsPath(target); - - assertEquals(expected, vfsPath + ":" + txt.getTextOffset()); - } // - else if (target instanceof LeafPsiElement leaf) { - String vfsPath = XmlPsiHelper.getNopVfsPath(target); - - assertEquals(expected, vfsPath + "#" + leaf.getText()); - } // - else { - fail("Unknown target " + target.getClass()); - } - } - } -} -- Gitee From 120eea83350a91bded760e3a506a2928b691c31c Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 11 Jul 2025 15:45:06 +0800 Subject: [PATCH 48/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7=E7=9A=84=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E8=AF=86=E5=88=AB=E9=80=BB=E8=BE=91=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B7=B2=E7=9F=A5=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../idea/plugin/lang/psi/XLangAttribute.java | 39 +++-- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 53 +++++-- .../reference/XLangAttributeReference.java | 55 +++++-- .../{ => lang}/reference/XLangReference.java | 2 +- .../reference/XLangElementReference.java | 1 + .../reference/XLangNotFoundReference.java | 1 + .../reference/XLangVfsFileReference.java | 1 + .../plugin/reference/XLangXDefReference.java | 1 + .../idea/plugin/BaseXLangPluginTestCase.java | 2 +- .../idea/plugin/lang/TestXLangReferences.java | 141 +++++++++++------- .../test/resources/_vfs/test/doc/example.xdoc | 7 +- 11 files changed, 210 insertions(+), 93 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/{ => lang}/reference/XLangReference.java (75%) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java index d67983d4f..9b3d90557 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -3,8 +3,9 @@ package io.nop.idea.plugin.lang.psi; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; +import com.intellij.psi.impl.source.xml.SchemaPrefixReference; import com.intellij.psi.impl.source.xml.XmlAttributeImpl; -import com.intellij.psi.xml.XmlElement; +import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.lang.reference.XLangAttributeReference; import io.nop.xlang.xdef.IXDefAttribute; import org.jetbrains.annotations.NotNull; @@ -24,16 +25,24 @@ public class XLangAttribute extends XmlAttributeImpl { @Override public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { - XmlElement attrName = getNameElement(); - if (attrName == null) { - return PsiReference.EMPTY_ARRAY; + // 参考 XmlAttributeDelegate#getDefaultReferences + String ns = getNamespacePrefix(); + String name = getLocalName(); + + if (name.isEmpty() || isNamespaceDeclaration()) { + return super.getReferences(hints); } - TextRange textRange = attrName.getTextRangeInParent(); - XLangAttributeReference ref = new XLangAttributeReference(this, textRange); + // 保留对名字空间的引用,以支持对其做高亮、重命名等 + SchemaPrefixReference ref0 = !ns.isEmpty() + ? new SchemaPrefixReference(this, TextRange.allOf(ns), ns, null) + : null; + + int nameOffset = (ns.isEmpty() ? -1 : ns.length()) + 1; + TextRange nameTextRange = TextRange.allOf(name).shiftRight(nameOffset); + XLangAttributeReference ref1 = new XLangAttributeReference(this, nameTextRange); - //return super.getReferences(hints); - return new PsiReference[] { ref }; + return ref0 != null ? new PsiReference[] { ref0, ref1 } : new PsiReference[] { ref1 }; } /** 获取当前属性的 xdef 定义 */ @@ -54,11 +63,11 @@ public class XLangAttribute extends XmlAttributeImpl { if (tag.isInXDef()) { // 取 xdef.xdef 中声明的属性 - if (attrName.startsWith(xdefNs + ':')) { + if (StringHelper.startsWithNamespace(attrName, xdefNs)) { attrDef = tag.getXDefNodeAttr(attrName); } // 取 xdsl.xdef 中声明的属性 - else if (attrName.startsWith(xdslNs + ':')) { + else if (StringHelper.startsWithNamespace(attrName, xdslNs)) { attrDef = tag.getXDslDefNodeAttr(attrName); } // 取自身声明的属性 @@ -66,7 +75,7 @@ public class XLangAttribute extends XmlAttributeImpl { attrDef = tag.getSelfDefNodeAttr(attrName); } } else { - if (attrName.startsWith(xdslNs + ':')) { + if (StringHelper.startsWithNamespace(attrName, xdslNs)) { attrDef = tag.getXDslDefNodeAttr(attrName); } else { attrDef = tag.getXDefNodeAttr(attrName); @@ -75,4 +84,12 @@ public class XLangAttribute extends XmlAttributeImpl { return attrDef; } + + /** 是否为当前属性自身的 xdef 定义 */ + public boolean isSelfDefAttr(IXDefAttribute attr) { + String attrName = getName(); + XLangTag tag = (XLangTag) getParent(); + + return tag != null && attr == tag.getSelfDefNodeAttr(attrName); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index ccce9fc72..eea7b3b8c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -4,6 +4,7 @@ import com.intellij.openapi.project.Project; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.XmlTagImpl; +import io.nop.commons.util.StringHelper; import io.nop.core.lang.xml.XNode; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; @@ -12,6 +13,8 @@ import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefinition; import io.nop.xlang.xdef.XDefKeys; +import io.nop.xlang.xdef.XDefTypeDecl; +import io.nop.xlang.xdef.impl.XDefAttribute; import io.nop.xlang.xdsl.XDslConstants; import io.nop.xlang.xdsl.XDslKeys; import org.jetbrains.annotations.NotNull; @@ -117,19 +120,45 @@ public class XLangTag extends XmlTagImpl { /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ private static IXDefAttribute getXDefNodeAttr(IXDefNode xdefNode, String attrName) { - IXDefAttribute attr = xdefNode != null ? xdefNode.getAttribute(attrName) : null; + if (xdefNode == null) { + return null; + } + + IXDefAttribute attr = xdefNode.getAttribute(attrName); + if (attr != null) { + return attr; + } + + // Note: 在普通 *.xdef 的 IXDefNode 中, + // 对 xdef:unknown-attr 只记录了类型,并没有 IXDefAttribute 实体, + // 其处理逻辑见 XDefinitionParser#parseNode + XDefTypeDecl xdefUnknownAttrType = xdefNode.getXdefUnknownAttr(); + if (xdefUnknownAttrType != null) { + XDefAttribute at = new XDefAttribute() { + @Override + public boolean isUnknownAttr() { + return true; + } + }; - return attr; + at.setName(XDefKeys.DEFAULT.UNKNOWN_ATTR); + at.setType(xdefUnknownAttrType); + // Note: 在需要时,通过节点位置再定位具体的属性位置 + at.setLocation(xdefNode.getLocation()); + + return at; + } + + return null; } private static String changeNamespace(String name, String fromNs, String toNs) { - if (fromNs.equals(toNs)) { + if (fromNs == null || toNs == null || fromNs.equals(toNs)) { return name; } - String ns = fromNs + ':'; - if (name.startsWith(ns)) { - return toNs + ':' + name.substring(ns.length()); + if (StringHelper.startsWithNamespace(name, fromNs)) { + return toNs + ':' + name.substring(fromNs.length() + 1); } return name; } @@ -188,16 +217,22 @@ public class XLangTag extends XmlTagImpl { return SchemaMeta.UNDEFINED; } + String tagName = getName(); String xdefNs = parentTag.getXDefNs(); String xdslNs = parentTag.getXDslNs(); IXDefNode parentXDefNode = parentTag.getXDefNode(); IXDefNode parentXDslDefNode = parentTag.getXDslDefNode(); IXDefNode parentSelfDefNode = parentTag.getSelfDefNode(); - String tagName = getName(); - tagName = changeNamespace(tagName, xdefNs, XDefKeys.DEFAULT.NS); + // Note: 如果是 xdef.xdef 中的节点,则其节点 xdef 定义均为 xdef:unknown-tag + boolean inXDefXDef = xdef.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // + && !XDefKeys.DEFAULT.NS.equals(xdefNs); // 在单元测试中只能基于内容做判断,而不是 vfs 路径 + IXDefNode xdefNode = parentXDefNode != null // + ? inXDefXDef // + ? parentXDefNode.getXdefUnknownTag() // + : parentXDefNode.getChild(tagName) // + : null; - IXDefNode xdefNode = parentXDefNode != null ? parentXDefNode.getChild(tagName) : null; IXDefNode xdslDefNode = parentXDslDefNode != null ? parentXDslDefNode.getChild(tagName) : null; IXDefNode selfDefNode = parentSelfDefNode != null ? parentSelfDefNode.getChild(tagName) : null; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java index 392882c89..bce842816 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java @@ -4,19 +4,23 @@ import java.util.Objects; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementResolveResult; +import com.intellij.psi.PsiPolyVariantReference; +import com.intellij.psi.ResolveResult; import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlTag; import io.nop.api.core.util.SourceLocation; import io.nop.idea.plugin.lang.psi.XLangAttribute; -import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.xlang.xdef.IXDefAttribute; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author flytreeleft * @date 2025-07-10 */ -public class XLangAttributeReference extends XLangReferenceBase { +public class XLangAttributeReference extends XLangReferenceBase implements PsiPolyVariantReference { public XLangAttributeReference(XLangAttribute myElement, TextRange myRangeInElement) { super(myElement, myRangeInElement); @@ -24,25 +28,50 @@ public class XLangAttributeReference extends XLangReferenceBase { @Override public @Nullable PsiElement resolveInner() { + ResolveResult[] results = multiResolve(false); + + return results.length > 0 ? results[0].getElement() : null; + } + + /** 返回多个引用元素 */ + @Override + public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { XLangAttribute attr = (XLangAttribute) myElement; IXDefAttribute attrDef = attr.getXDefAttr(); if (attrDef == null) { - return null; + return ResolveResult.EMPTY_ARRAY; + } + + // 若引用属性自身,则直接返回 + if (attr.isSelfDefAttr(attrDef)) { + return new ResolveResult[] { + new PsiElementResolveResult(attr) + }; } - XLangTag tag = (XLangTag) attr.getParent(); SourceLocation loc = attrDef.getLocation(); + if (loc == null) { + return ResolveResult.EMPTY_ARRAY; + } + // Note: SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 - String path = loc != null // - ? loc.getPath().replace("classpath:_vfs", "") // - : tag.getXDef().resourcePath(); + String path = loc.getPath().replace("classpath:_vfs", ""); + + return XmlPsiHelper.findPsiFilesByNopVfsPath(attr, path).stream() // + .map((file) -> { + PsiElement target = XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class); - PsiElement[] targets = XmlPsiHelper.findPsiFilesByNopVfsPath(attr, path) - .stream() - .map((file) -> XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class)) - .filter(Objects::nonNull) - .toArray(PsiElement[]::new); + if (target == null) { + target = XmlPsiHelper.getPsiElementAt(file, loc, XmlTag.class); - return targets[0]; + if (target instanceof XmlTag t) { + target = t.getAttribute(attrDef.getName()); + } + } + return target; + }) // + .filter(Objects::nonNull) // + .map(PsiElementResolveResult::new) // + .toArray(ResolveResult[]::new); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java similarity index 75% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReference.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java index 35cf1206b..e78539281 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java @@ -1,4 +1,4 @@ -package io.nop.idea.plugin.reference; +package io.nop.idea.plugin.lang.reference; /** * @author flytreeleft diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java index a5a38c90c..c7abfb3c0 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangElementReference.java @@ -5,6 +5,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.reference.XLangReference; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java index b747ab6a4..612255a9b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java @@ -5,6 +5,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.xml.XmlElement; import com.intellij.util.ArrayUtil; +import io.nop.idea.plugin.lang.reference.XLangReference; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java index d69937d1b..564b86b5e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java @@ -8,6 +8,7 @@ import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlTag; import com.intellij.util.ArrayUtil; +import io.nop.idea.plugin.lang.reference.XLangReference; import io.nop.idea.plugin.utils.XmlPsiHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java index b10c0f8d5..3c294b52a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java @@ -5,6 +5,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiReferenceBase; import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.reference.XLangReference; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index c8b199243..ca53ad107 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -36,7 +36,7 @@ import io.nop.core.resource.IResource; import io.nop.core.resource.ResourceHelper; import io.nop.core.resource.impl.ClassPathResource; import io.nop.idea.plugin.lang.XLangFileType; -import io.nop.idea.plugin.reference.XLangReference; +import io.nop.idea.plugin.lang.reference.XLangReference; import io.nop.idea.plugin.services.NopAppListener; /** diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 4aa28b657..e0603e35a 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -1,19 +1,12 @@ package io.nop.idea.plugin.lang; -import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiPlainText; import com.intellij.psi.PsiReference; -import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.impl.source.xml.SchemaPrefix; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlTag; import io.nop.idea.plugin.BaseXLangPluginTestCase; -import io.nop.idea.plugin.reference.XLangElementReference; -import io.nop.idea.plugin.reference.XLangReference; -import io.nop.idea.plugin.reference.XLangVfsFileReference; -import io.nop.idea.plugin.reference.XLangXDefReference; import io.nop.idea.plugin.utils.XmlPsiHelper; /** @@ -36,23 +29,40 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { } public void testAttributeReferences() { - // 名字空间不做引用 assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), null); - + "meta:unique-attr=\"name\""), "meta"); assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", "meta:unique-attr=\"name\""), - "meta:define#xdef:unique-attr=xml-name"); + "/nop/schema/xdef.xdef?meta:define#xdef:unique-attr=xml-name"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), "xdef:prop#name=!xml-name"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("ef:name=\"!var-name\""), + "xdef"); assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("ame=\"!var-name\""), "xdef:define#xdef:name=!var-name"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"XDefNode\""), + "/nop/schema/xdef.xdef?meta:define#xdef:name=var-name"); + + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=", "xdsl:schema="), + "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:schema=v-path"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", + "x:schema=\"v-path\""), + "xdef:unknown-tag#x:schema=v-path"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:allow-multiple=\"true\"", + "xdef:allow-multiple=\"true\""), + "/nop/schema/xdef.xdef?meta:define#xdef:allow-multiple=boolean"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("x:key-attr=\"xml-name\"", + "x:key-attr=\"xml-name\""), + "xdef:unknown-tag#x:key-attr=xml-name"); assertReference(readVfsResource("/test/doc/example.xdef").replace("me=\"string\""), - "meta:define#xdef:unknown-attr=def-type"); + "child#name=string"); assertReference(""" pe="leaf"/> - """, "child#type=dict:test/doc/child-type"); + """, "/test/doc/example.xdef?child#type=dict:test/doc/child-type=node"); assertReference(""" ge="22"/> - """, "child#xdef:unknown-attr=any"); + """, "/test/doc/example.xdef?child#xdef:unknown-attr=any"); assertReference(""" ge="23"/> - """, "xdef:unknown-tag#xdef:unknown-attr=any"); + """, "/test/doc/example.xdef?xdef:unknown-tag#xdef:unknown-attr=any"); + assertReference(""" + + ="aaa"/> + + """, null); assertReference(""" f="/test/reference/test-filter.xdef#FilterCondition" /> - """, "schema#ref=xdef-ref"); + """, "/nop/schema/schema/schema-node.xdef?schema#ref=xdef-ref"); } public void testAttributeValueReferences() { @@ -401,54 +418,66 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { configureByXLangText(text); PsiReference ref = findReferenceAtCaret(); + PsiElement target = ref != null ? ref.resolve() : null; -// if (!(ref instanceof XLangReference) && expected == null) { -// return; // 不检查非 XLang 引用 -// } -// assertInstanceOf(ref, XLangReference.class); - - PsiElement target = ref.resolve(); if (expected == null) { assertNull(target); return; } assertNotNull(target); - if (ref instanceof XLangVfsFileReference) { - // Note: 可能不是 vfs 文件 - String vfsPath = XmlPsiHelper.getNopVfsPath(target); - String anchor = target instanceof XmlAttribute attr ? attr.getValue() : null; + // Note: 可能不是 vfs 文件 + String vfsPath = XmlPsiHelper.getNopVfsPath(target); - assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); - } // - else if (ref instanceof XLangElementReference || ref instanceof XLangXDefReference) { - if (target instanceof XmlTag tag) { - assertEquals(expected, tag.getName()); - } // - else if (target instanceof XmlAttribute attr) { - XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); + if (target instanceof XmlAttribute attr) { + XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); - assertEquals(expected, tag.getName() + "#" + attr.getName() + "=" + attr.getValue()); - } // - else if (target instanceof PsiClass cls) { - assertEquals(expected, cls.getQualifiedName()); - } // - else if (target instanceof PsiField field) { - assertEquals(expected, field.getContainingClass().getQualifiedName() + "#" + field.getName()); - } // - else if (target instanceof PsiPlainText txt) { - String vfsPath = XmlPsiHelper.getNopVfsPath(target); - - assertEquals(expected, vfsPath + ":" + txt.getTextOffset()); - } // - else if (target instanceof LeafPsiElement leaf) { - String vfsPath = XmlPsiHelper.getNopVfsPath(target); - - assertEquals(expected, vfsPath + "#" + leaf.getText()); - } // - else { - fail("Unknown target " + target.getClass()); - } + assertEquals(expected, + (vfsPath != null ? vfsPath + '?' : "") + + tag.getName() + + '#' + + attr.getName() + + '=' + + attr.getValue()); + } else if (target instanceof SchemaPrefix ns) { + assertEquals(expected, ns.getName()); } + +// if (ref instanceof XLangVfsFileReference) { +// // Note: 可能不是 vfs 文件 +// String vfsPath = XmlPsiHelper.getNopVfsPath(target); +// String anchor = target instanceof XmlAttribute attr ? attr.getValue() : null; +// +// assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); +// } // +// else if (ref instanceof XLangElementReference || ref instanceof XLangXDefReference) { +// if (target instanceof XmlTag tag) { +// assertEquals(expected, tag.getName()); +// } // +// else if (target instanceof XmlAttribute attr) { +// XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); +// +// assertEquals(expected, tag.getName() + "#" + attr.getName() + "=" + attr.getValue()); +// } // +// else if (target instanceof PsiClass cls) { +// assertEquals(expected, cls.getQualifiedName()); +// } // +// else if (target instanceof PsiField field) { +// assertEquals(expected, field.getContainingClass().getQualifiedName() + "#" + field.getName()); +// } // +// else if (target instanceof PsiPlainText txt) { +// String vfsPath = XmlPsiHelper.getNopVfsPath(target); +// +// assertEquals(expected, vfsPath + ":" + txt.getTextOffset()); +// } // +// else if (target instanceof LeafPsiElement leaf) { +// String vfsPath = XmlPsiHelper.getNopVfsPath(target); +// +// assertEquals(expected, vfsPath + "#" + leaf.getText()); +// } // +// else { +// fail("Unknown target " + target.getClass()); +// } +// } } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc index 8eaacea08..2b848b428 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc @@ -1,8 +1,11 @@ - + /test/reference/test-filter.xdef, - /nop/schema/xdsl.xdef + /nop/schema/xdsl.xdef + + + -- Gitee From 6d6e5775e71bed9537e16d8ae11390379540a345 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 11 Jul 2025 21:32:33 +0800 Subject: [PATCH 49/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AF=B9=20Xpl=20=E8=8A=82=E7=82=B9=E5=B1=9E=E6=80=A7=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../idea/plugin/lang/psi/XLangAttribute.java | 28 +++---- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 83 +++++++++++-------- .../nop/idea/plugin/utils/XDefPsiHelper.java | 24 +----- .../idea/plugin/lang/TestXLangReferences.java | 50 +++++++++++ 4 files changed, 115 insertions(+), 70 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java index 9b3d90557..a39e7ccdd 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -5,7 +5,6 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.SchemaPrefixReference; import com.intellij.psi.impl.source.xml.XmlAttributeImpl; -import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.lang.reference.XLangAttributeReference; import io.nop.xlang.xdef.IXDefAttribute; import org.jetbrains.annotations.NotNull; @@ -52,34 +51,31 @@ public class XLangAttribute extends XmlAttributeImpl { return null; } - // - 对于声明属性,从其自身(*.xdef)中取其定义 - // - 对于赋值属性,从其 x:schema 中取其定义 + // - 对于声明属性(定义属性名及其类型),从其自身(*.xdef)中取其定义 + // - 对于赋值属性(为具体属性赋予相应类型的值),从其 x:schema 中取其定义 // - 对于名字空间对应 xdsl.xdef 和 xdef.xdef 的属性,则分别从这两个元模型中取属性定义 IXDefAttribute attrDef; + String ns = getNamespacePrefix(); String attrName = getName(); - String xdefNs = tag.getXDefNs(); - String xdslNs = tag.getXDslNs(); + boolean hasXDefNs = !ns.isEmpty() && ns.equals(tag.getXDefNs()); + boolean hasXDslNs = !ns.isEmpty() && ns.equals(tag.getXDslNs()); - if (tag.isInXDef()) { + // 取 xdsl.xdef 中声明的属性 + if (hasXDslNs) { + attrDef = tag.getXDslDefNodeAttr(attrName); + } // + else if (tag.isInXDef()) { // 取 xdef.xdef 中声明的属性 - if (StringHelper.startsWithNamespace(attrName, xdefNs)) { + if (hasXDefNs) { attrDef = tag.getXDefNodeAttr(attrName); } - // 取 xdsl.xdef 中声明的属性 - else if (StringHelper.startsWithNamespace(attrName, xdslNs)) { - attrDef = tag.getXDslDefNodeAttr(attrName); - } // 取自身声明的属性 else { attrDef = tag.getSelfDefNodeAttr(attrName); } } else { - if (StringHelper.startsWithNamespace(attrName, xdslNs)) { - attrDef = tag.getXDslDefNodeAttr(attrName); - } else { - attrDef = tag.getXDefNodeAttr(attrName); - } + attrDef = tag.getXDefNodeAttr(attrName); } return attrDef; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index eea7b3b8c..9fb446918 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -64,15 +64,13 @@ public class XLangTag extends XmlTagImpl { } /** @see SchemaMeta#xdef */ - public IXDefinition getXDef() { - prepareSchema(); - return schemaMeta.xdef; + private IXDefinition getXDef() { + return getSchemaMeta().xdef; } /** @see SchemaMeta#xdefNode */ public IXDefNode getXDefNode() { - prepareSchema(); - return schemaMeta.xdefNode; + return getSchemaMeta().xdefNode; } public IXDefAttribute getXDefNodeAttr(String attrName) { @@ -84,22 +82,19 @@ public class XLangTag extends XmlTagImpl { /** @see SchemaMeta#xdslDefNode */ public IXDefNode getXDslDefNode() { - prepareSchema(); - return schemaMeta.xdslDefNode; + return getSchemaMeta().xdslDefNode; } public IXDefAttribute getXDslDefNodeAttr(String attrName) { // Note: xdsl.xdef 的属性在固定的名字空间 x 中声明 - String xNs = XDslKeys.DEFAULT.X_NS_PREFIX.substring(0, XDslKeys.DEFAULT.X_NS_PREFIX.length() - 1); - attrName = changeNamespace(attrName, getXDslNs(), xNs); + attrName = changeNamespace(attrName, getXDslNs(), XDslKeys.DEFAULT.NS); return getXDefNodeAttr(getXDslDefNode(), attrName); } /** @see SchemaMeta#selfDefNode */ public IXDefNode getSelfDefNode() { - prepareSchema(); - return schemaMeta.selfDefNode; + return getSchemaMeta().selfDefNode; } public IXDefAttribute getSelfDefNodeAttr(String attrName) { @@ -108,14 +103,12 @@ public class XLangTag extends XmlTagImpl { /** @see SchemaMeta#xdefNs */ public String getXDefNs() { - prepareSchema(); - return schemaMeta.xdefNs; + return getSchemaMeta().xdefNs; } /** @see SchemaMeta#xdslNs */ public String getXDslNs() { - prepareSchema(); - return schemaMeta.xdslNs; + return getSchemaMeta().xdslNs; } /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ @@ -163,13 +156,13 @@ public class XLangTag extends XmlTagImpl { return name; } - private synchronized void prepareSchema() { - if (this.schemaMeta != null) { - return; - } + private synchronized SchemaMeta getSchemaMeta() { + if (schemaMeta == null) { + Project project = getProject(); - Project project = getProject(); - schemaMeta = ProjectEnv.withProject(project, this::createSchemaMeta); + schemaMeta = ProjectEnv.withProject(project, this::createSchemaMeta); + } + return schemaMeta; } private SchemaMeta createSchemaMeta() { @@ -179,12 +172,12 @@ public class XLangTag extends XmlTagImpl { if (parentTag == null) { String schemaUrl = XDefPsiHelper.getSchemaPath(this); if (schemaUrl == null) { - return SchemaMeta.UNDEFINED; + return SchemaMeta.UNKNOWN; } IXDefinition xdef = XDefPsiHelper.loadSchema(schemaUrl); if (xdef == null) { - return SchemaMeta.UNDEFINED; + return SchemaMeta.UNKNOWN; } String xdefNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDEF); @@ -214,7 +207,7 @@ public class XLangTag extends XmlTagImpl { IXDefinition xdef = parentTag.getXDef(); if (xdef == null) { - return SchemaMeta.UNDEFINED; + return SchemaMeta.UNKNOWN; } String tagName = getName(); @@ -224,18 +217,42 @@ public class XLangTag extends XmlTagImpl { IXDefNode parentXDslDefNode = parentTag.getXDslDefNode(); IXDefNode parentSelfDefNode = parentTag.getSelfDefNode(); - // Note: 如果是 xdef.xdef 中的节点,则其节点 xdef 定义均为 xdef:unknown-tag - boolean inXDefXDef = xdef.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // - && !XDefKeys.DEFAULT.NS.equals(xdefNs); // 在单元测试中只能基于内容做判断,而不是 vfs 路径 - IXDefNode xdefNode = parentXDefNode != null // - ? inXDefXDef // - ? parentXDefNode.getXdefUnknownTag() // - : parentXDefNode.getChild(tagName) // - : null; + boolean xpl = XDefPsiHelper.isXplDefNode(parentXDefNode) // + // xlib.xdef 中的 source 标签设置为 xml 类型,是因为在获取 XplLib 模型的时候会根据 xlib.xdef 来解析, + // 但此时这个 source 段无法自动进行编译,必须结合它的 outputMode 和 attrs 配置等才能决定。 + // 因此,将其子节点同样视为 xpl 节点处理 + || ((xdef.resourcePath().equals(XDslConstants.XDSL_SCHEMA_XLIB) // + || xdef.resourcePath().endsWith("_vfs" + XDslConstants.XDSL_SCHEMA_XLIB) // + ) // + && "xml".equals(XDefPsiHelper.getDefNodeType(parentXDefNode)) // + && "source".equals(parentTag.getName()) // + ); + if (xpl) { + // 对于 Xpl 节点,始终采用 xpl.xdef 作为其子节点的元模型 + xdef = XDefPsiHelper.getXplDef(); + // Xpl 子节点均为 xdef:unknown-tag + parentXDefNode = xdef.getRootNode().getXdefUnknownTag(); + } IXDefNode xdslDefNode = parentXDslDefNode != null ? parentXDslDefNode.getChild(tagName) : null; IXDefNode selfDefNode = parentSelfDefNode != null ? parentSelfDefNode.getChild(tagName) : null; + IXDefNode xdefNode; + if (tagName.startsWith(XDslKeys.DEFAULT.X_NS_PREFIX)) { + xdef = XDefPsiHelper.getXDslDef(); + xdefNode = xdslDefNode; + } else { + // Note: 如果是 xdef.xdef 中的节点,则其节点 xdef 定义均为 xdef:unknown-tag + boolean inXDefXDef = xdef.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // + && !XDefKeys.DEFAULT.NS.equals(xdefNs); // 在单元测试中只能基于内容做判断,而不是 vfs 路径 + + xdefNode = parentXDefNode != null // + ? inXDefXDef // + ? parentXDefNode.getXdefUnknownTag() // + : parentXDefNode.getChild(tagName) // + : null; + } + return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefNs, xdslNs); } @@ -262,6 +279,6 @@ public class XLangTag extends XmlTagImpl { IXDefNode selfDefNode, // String xdefNs, String xdslNs // ) { - public static final SchemaMeta UNDEFINED = new SchemaMeta(null, null, null, null, null, null); + public static final SchemaMeta UNKNOWN = new SchemaMeta(null, null, null, null, null, null); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 5f6c6374e..d7a55e713 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -135,24 +135,6 @@ public class XDefPsiHelper { } public static XmlTagInfo getTagInfo(String schemaUrl, XmlTag tag) { - // 在对应解析 xml 标签的元模型节点时,需要注意以下几点 - // - 若根节点的 `x:schema` 为 `/nop/schema/xdef.xdef`,则其在定义某类 DSL 的元模型, - // 若引用的是其他 xdef,则其是在定义某个具体的 DSL 模型 - // - 在普通的 XDef 元模型中,必须在根节点固定定义 `xdef` 和 `x` 名字空间: - // - `xmlns:x="/nop/schema/xdsl.xdef"` - // - `xmlns:xdef="/nop/schema/xdef.xdef"` - // 其中,以 `xdef` 为名字空间的节点和属性,用于定义 DSL 的结构,而以 - // `x` 为名字空间的节点和属性,则用于定义差量规则 - // - 而在普通的 DSL 模型中,必须在根节点固定定义 `x` 名字空间: - // - `xmlns:x="/nop/schema/xdsl.xdef"` - // 其中,以 `x` 为名字空间的节点和属性,则用于定义差量规则 - // - `/nop/schema/xdef.xdef` 本身的定义是**自举**的,其描述的是所有元模型的结构。 - // 其以 `meta` 作为名字空间来定义其 DSL 结构, - // 而以 `xdef` 为名字空间的属性和节点,则为其 DSL 的**元属性**和**元节点**, - // 用于声明其 DSL 模型所包含的属性和节点类型 - // - `/nop/schema/xdsl.xdef` 自身的定义也是**自举**的,其描述的是所有 DSL 模型的结构。 - // 其以 `xdsl` 作为名字空间,对其进行差量控制(这里主要为指定其 `schema` 为 `/nop/schema/xdef.xdef`) - // - `/nop/schema/xpl.xdef` 为 Xpl 类型节点的元模型,且以 `xpl` 为其固定的名字空间 IXDefinition def = loadSchema(schemaUrl); if (def == null) { return null; @@ -198,7 +180,7 @@ public class XDefPsiHelper { tagInfo = new XmlTagInfo(xmlTag, parentTagInfo, def, defNode, xdslDefNode, xdefNs, xdslNs); - if (isXplNode(defNode)) { + if (isXplDefNode(defNode)) { xpl = true; } // xlib.xdef 中的 source 标签设置为 xml 类型,是因为在获取 XplLib 模型的时候会根据 xlib.xdef 来解析, @@ -229,13 +211,13 @@ public class XDefPsiHelper { return xmlName; } - static boolean isXplNode(IXDefNode defNode) { + public static boolean isXplDefNode(IXDefNode defNode) { String stdDomain = getDefNodeType(defNode); return stdDomain != null && (stdDomain.equals("xpl") || stdDomain.startsWith("xpl-")); } - static String getDefNodeType(IXDefNode defNode) { + public static String getDefNodeType(IXDefNode defNode) { if (defNode == null || defNode.getXdefValue() == null) { return null; } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index e0603e35a..4a25995a8 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -29,6 +29,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { } public void testAttributeReferences() { + // xdef.xdef 中的引用识别 assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", "meta:unique-attr=\"name\""), "meta"); assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", @@ -48,6 +49,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { "me=\"XDefNode\""), "/nop/schema/xdef.xdef?meta:define#xdef:name=var-name"); + // xdsl.xdef 中的引用识别 assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=", "xdsl:schema="), "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:schema=v-path"); assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", @@ -60,6 +62,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { "x:key-attr=\"xml-name\""), "xdef:unknown-tag#x:key-attr=xml-name"); + // assertReference(readVfsResource("/test/doc/example.xdef").replace("me=\"string\""), "child#name=string"); @@ -99,6 +102,53 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { ref="/test/reference/test-filter.xdef#FilterCondition" /> """, "/nop/schema/schema/schema-node.xdef?schema#ref=xdef-ref"); + + // 对 Xpl 属性的引用识别 + assertReference(""" + + +
Mode="node"/> + + + """, + "/nop/schema/xpl.xdef?xdef:define#xpl:outputMode=enum:io.nop.xlang.ast.XLangOutputMode"); + assertReference(""" + + +
+ f="true"/> +
+
+ + """, null); + assertReference(""" + + +
+ + f="true"/> + +
+
+ + """, "/nop/schema/xpl.xdef?xdef:define#xpl:if=expr"); + assertReference(""" + + + + + f="a > b"/> + + + + + """, "/nop/schema/xpl.xdef?xdef:define#xpl:if=expr"); } public void testAttributeValueReferences() { -- Gitee From b8cc871c0e70621c86d8f3e3eecb7a0a35845a2d Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 11 Jul 2025 21:33:28 +0800 Subject: [PATCH 50/82] =?UTF-8?q?nop-xlang:=20=E6=96=B0=E5=A2=9E=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=20XDslKeys#NS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java b/nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java index b8816679a..0beb1f5b4 100644 --- a/nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java +++ b/nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java @@ -21,6 +21,7 @@ public final class XDslKeys implements Serializable { public static final XDslKeys XPL = new XDslKeys("xpl"); + public final String NS; public final String X_NS_PREFIX; public final String SCHEMA; @@ -61,6 +62,7 @@ public final class XDslKeys implements Serializable { public final Set CHILD_NAMES; public XDslKeys(String ns) { + this.NS = ns; this.X_NS_PREFIX = ns + ':'; this.SCHEMA = getFullName(ns, "schema"); this.ID = getFullName(ns, "id"); @@ -115,4 +117,4 @@ public final class XDslKeys implements Serializable { return DEFAULT; return new XDslKeys(ns); } -} \ No newline at end of file +} -- Gitee From 76610da7739c1775032b14279fb0a514fd404a47 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 13 Jul 2025 18:10:51 +0800 Subject: [PATCH 51/82] =?UTF-8?q?nop-xlang:=20=E8=A1=A5=E5=85=85=E8=BE=85?= =?UTF-8?q?=E5=8A=A9=E6=96=B9=E6=B3=95=20XDslKeys#of(String)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java | 4 ++-- nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java b/nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java index da9f7e745..ca85809ff 100644 --- a/nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java +++ b/nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java @@ -22,7 +22,7 @@ public class XDefKeys implements Serializable { public static XDefKeys of(XNode node) { String ns = node.getXmlnsForUrl(XDslConstants.XDSL_SCHEMA_XDEF); - if (ns == null || ns.equals("xdef")) + if (ns == null || ns.equals(DEFAULT.NS)) return DEFAULT; return new XDefKeys(ns); } @@ -187,4 +187,4 @@ public class XDefKeys implements Serializable { private static String getFullName(String ns, String name) { return ns + ":" + name; } -} \ No newline at end of file +} diff --git a/nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java b/nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java index 0beb1f5b4..bbaac5fc7 100644 --- a/nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java +++ b/nop-xlang/src/main/java/io/nop/xlang/xdsl/XDslKeys.java @@ -113,7 +113,11 @@ public final class XDslKeys implements Serializable { public static XDslKeys of(XNode node) { String ns = node.getXmlnsForUrl(XDslConstants.XDSL_SCHEMA_XDSL); - if (ns == null || ns.equals("x")) + return of(ns); + } + + public static XDslKeys of(String ns) { + if (ns == null || ns.equals(DEFAULT.NS)) return DEFAULT; return new XDslKeys(ns); } -- Gitee From ff110b1707c8b2e437fa92859b18e1c114f4f220 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 13 Jul 2025 18:11:21 +0800 Subject: [PATCH 52/82] =?UTF-8?q?nop-idea-pugin:=20=E5=88=9D=E6=AD=A5?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E5=80=BC=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 16 +- .../nop/idea/plugin/lang/XLangLanguage.java | 4 +- .../idea/plugin/lang/psi/XLangAttribute.java | 12 +- .../plugin/lang/psi/XLangAttributeValue.java | 72 +++++++ .../io/nop/idea/plugin/lang/psi/XLangTag.java | 59 ++++-- ...erence.java => XLangDefAttrReference.java} | 64 +++--- .../reference/XLangDictOptionReference.java | 100 +++++++++ .../XLangParentTagAttrReference.java | 39 ++++ .../plugin/lang/reference/XLangReference.java | 8 +- .../lang/reference/XLangReferenceBase.java | 13 +- .../lang/reference/XLangReferenceHelper.java | 189 ++++++++++++++++++ .../XLangStdDomainXdefRefReference.java | 89 +++++++++ .../reference/XLangXPrototypeReference.java | 66 ++++++ .../reference/XLangXdefKeyAttrReference.java | 51 +++++ .../lang/script/psi/ExpressionNode.java | 4 +- .../script/psi/ObjectDeclarationNode.java | 2 +- .../script/psi/QualifiedNameRootNode.java | 2 +- .../psi/TypeNameNodePredefinedNode.java | 2 +- .../script/reference/IdentifierReference.java | 2 +- .../reference/PredefinedTypeReference.java | 2 +- .../reference/QualifiedNameReference.java | 4 +- .../reference/XLangReferenceContributor.java | 43 ---- .../nop/idea/plugin/vfs/NopVirtualFile.java | 177 ++++++++++++++++ .../plugin/vfs/NopVirtualFileReference.java | 43 ++++ .../src/main/resources/META-INF/plugin.xml | 2 - .../messages/NopPluginBundle.properties | 5 +- .../messages/NopPluginBundle_zh.properties | 5 +- .../idea/plugin/lang/TestXLangReferences.java | 36 ++-- 28 files changed, 963 insertions(+), 148 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/{XLangAttributeReference.java => XLangDefAttrReference.java} (37%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceContributor.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index 09454f935..6887a73e5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -27,6 +27,7 @@ import io.nop.api.core.validate.IValidationErrorCollector; import io.nop.commons.util.StringHelper; import io.nop.core.dict.DictProvider; import io.nop.core.exceptions.ErrorMessageManager; +import io.nop.idea.plugin.lang.reference.XLangReference; import io.nop.idea.plugin.messages.NopPluginBundle; import io.nop.idea.plugin.reference.XLangVfsFileReference; import io.nop.idea.plugin.reference.XLangElementReference; @@ -77,11 +78,16 @@ public class XLangAnnotator implements Annotator { .range(reference.getAbsoluteRange()) .textAttributes(DefaultLanguageHighlighterColors.DOC_COMMENT_TAG_VALUE) .create(); - } else if (reference instanceof XLangNotFoundReference ref) { - holder.newAnnotation(HighlightSeverity.ERROR, ref.getMessage()) - .range(ref.getAbsoluteRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); + } else if (reference instanceof XLangReference ref) { + ref.resolve(); // 确保已检查异常状态 + String msg = ref.getMessage(); + + if (msg != null) { + holder.newAnnotation(HighlightSeverity.ERROR, msg) + .range(ref.getAbsoluteRange()) + .highlightType(ProblemHighlightType.ERROR) + .create(); + } } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangLanguage.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangLanguage.java index 29bbe8ea2..bc5ff763d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangLanguage.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangLanguage.java @@ -20,6 +20,4 @@ public class XLangLanguage extends XMLLanguage { public XLangFileType getAssociatedFileType() { return XLangFileType.INSTANCE; } - - -} \ No newline at end of file +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java index a39e7ccdd..aa391164a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -5,7 +5,7 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.SchemaPrefixReference; import com.intellij.psi.impl.source.xml.XmlAttributeImpl; -import io.nop.idea.plugin.lang.reference.XLangAttributeReference; +import io.nop.idea.plugin.lang.reference.XLangDefAttrReference; import io.nop.xlang.xdef.IXDefAttribute; import org.jetbrains.annotations.NotNull; @@ -39,7 +39,7 @@ public class XLangAttribute extends XmlAttributeImpl { int nameOffset = (ns.isEmpty() ? -1 : ns.length()) + 1; TextRange nameTextRange = TextRange.allOf(name).shiftRight(nameOffset); - XLangAttributeReference ref1 = new XLangAttributeReference(this, nameTextRange); + XLangDefAttrReference ref1 = new XLangDefAttrReference(this, nameTextRange); return ref0 != null ? new PsiReference[] { ref0, ref1 } : new PsiReference[] { ref1 }; } @@ -58,8 +58,8 @@ public class XLangAttribute extends XmlAttributeImpl { String ns = getNamespacePrefix(); String attrName = getName(); - boolean hasXDefNs = !ns.isEmpty() && ns.equals(tag.getXDefNs()); - boolean hasXDslNs = !ns.isEmpty() && ns.equals(tag.getXDslNs()); + boolean hasXDefNs = !ns.isEmpty() && ns.equals(tag.getXDefKeys().NS); + boolean hasXDslNs = !ns.isEmpty() && ns.equals(tag.getXDslKeys().NS); // 取 xdsl.xdef 中声明的属性 if (hasXDslNs) { @@ -81,8 +81,8 @@ public class XLangAttribute extends XmlAttributeImpl { return attrDef; } - /** 是否为当前属性自身的 xdef 定义 */ - public boolean isSelfDefAttr(IXDefAttribute attr) { + /** 是否为声明属性(定义属性名及其类型) */ + public boolean isDeclaredDefAttr(IXDefAttribute attr) { String attrName = getName(); XLangTag tag = (XLangTag) getParent(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index d0271b28a..494354ac2 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -1,6 +1,19 @@ package io.nop.idea.plugin.lang.psi; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.XmlAttributeValueImpl; +import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.lang.reference.XLangReferenceHelper; +import io.nop.idea.plugin.lang.reference.XLangXdefKeyAttrReference; +import io.nop.idea.plugin.lang.reference.XLangParentTagAttrReference; +import io.nop.idea.plugin.lang.reference.XLangXPrototypeReference; +import io.nop.xlang.xdef.IXDefAttribute; +import io.nop.xlang.xdef.XDefKeys; +import io.nop.xlang.xdef.XDefTypeDecl; +import io.nop.xlang.xdsl.XDslKeys; +import org.jetbrains.annotations.NotNull; /** * 属性值,由引号和 {@link XLangValueToken} 组成 @@ -14,4 +27,63 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { public String toString() { return getClass().getSimpleName(); } + + @Override + public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { + String attrValue = getValue(); + if (StringHelper.isEmpty(attrValue)) { + return PsiReference.EMPTY_ARRAY; + } + + if (!(getParent() instanceof XLangAttribute attr)) { + return PsiReference.EMPTY_ARRAY; + } + + IXDefAttribute attrDef = attr.getXDefAttr(); + if (attrDef == null) { + return XLangReferenceHelper.getReferencesFromText(this, attrValue); + } + + XDefTypeDecl attrDefType = attrDef.getType(); + // 对于声明属性(定义属性名及其类型),仅对其类型的定义(涉及枚举和字典)做引用识别 + if (attr.isDeclaredDefAttr(attrDef)) { + return XLangReferenceHelper.getReferencesFromDefType(this, attrValue, attrDefType); + } + + // 根据属性的类型,对属性值做文件/名字引用 + PsiReference[] refs = XLangReferenceHelper.getReferencesByStdDomain(this, + attrValue, + attrDefType.getStdDomain()); + if (refs != null) { + return refs; + } + + XLangTag tag = (XLangTag) attr.getParent(); + XDslKeys xdslKeys = tag.getXDslKeys(); + XDefKeys xdefKeys = tag.getXDefKeys(); + + String attrName = attr.getName(); + // Note: XmlAttributeValue 的文本范围是包含引号的 + TextRange attrValueTextRange = getValueTextRange().shiftLeft(getStartOffset()); + if (xdslKeys.PROTOTYPE.equals(attrName)) { + return new PsiReference[] { + new XLangXPrototypeReference(this, attrValueTextRange, attrValue) + }; + } // + else if (xdefKeys.KEY_ATTR.equals(attrName)) { + return new PsiReference[] { + new XLangXdefKeyAttrReference(this, attrValueTextRange, attrValue) + }; + } // + else if (xdefKeys.UNIQUE_ATTR.equals(attrName)) { + return new PsiReference[] { + new XLangParentTagAttrReference(this, attrValueTextRange, attrValue) + }; + } + + // TODO 其他引用识别 + // + // + return XLangReferenceHelper.getReferencesFromText(this, attrValue); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 9fb446918..71b20ede1 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -1,5 +1,7 @@ package io.nop.idea.plugin.lang.psi; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; @@ -75,7 +77,7 @@ public class XLangTag extends XmlTagImpl { public IXDefAttribute getXDefNodeAttr(String attrName) { // Note: xdef.xdef 的属性在固定的名字空间 xdef 中声明 - attrName = changeNamespace(attrName, getXDefNs(), XDefKeys.DEFAULT.NS); + attrName = changeNamespace(attrName, getXDefKeys().NS, XDefKeys.DEFAULT.NS); return getXDefNodeAttr(getXDefNode(), attrName); } @@ -87,7 +89,7 @@ public class XLangTag extends XmlTagImpl { public IXDefAttribute getXDslDefNodeAttr(String attrName) { // Note: xdsl.xdef 的属性在固定的名字空间 x 中声明 - attrName = changeNamespace(attrName, getXDslNs(), XDslKeys.DEFAULT.NS); + attrName = changeNamespace(attrName, getXDslKeys().NS, XDslKeys.DEFAULT.NS); return getXDefNodeAttr(getXDslDefNode(), attrName); } @@ -101,14 +103,14 @@ public class XLangTag extends XmlTagImpl { return getXDefNodeAttr(getSelfDefNode(), attrName); } - /** @see SchemaMeta#xdefNs */ - public String getXDefNs() { - return getSchemaMeta().xdefNs; + /** @see SchemaMeta#xdefKeys */ + public XDefKeys getXDefKeys() { + return getSchemaMeta().xdefKeys; } - /** @see SchemaMeta#xdslNs */ - public String getXDslNs() { - return getSchemaMeta().xdslNs; + /** @see SchemaMeta#xdslKeys */ + public XDslKeys getXDslKeys() { + return getSchemaMeta().xdslKeys; } /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ @@ -159,8 +161,14 @@ public class XLangTag extends XmlTagImpl { private synchronized SchemaMeta getSchemaMeta() { if (schemaMeta == null) { Project project = getProject(); - schemaMeta = ProjectEnv.withProject(project, this::createSchemaMeta); + + try { + ProgressManager.checkCanceled(); + } catch (ProcessCanceledException e) { + // Note: 若处理被中断,则保持元模型信息为空,以便于后续再重新初始化 + schemaMeta = null; + } } return schemaMeta; } @@ -180,12 +188,13 @@ public class XLangTag extends XmlTagImpl { return SchemaMeta.UNKNOWN; } - String xdefNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDEF); + XDefKeys xdefKeys = xdef.getDefKeys(); String xdslNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDSL); + XDslKeys xdslKeys = XDslKeys.of(xdslNs); IXDefNode selfDefNode = null; // x:schema 为 /nop/schema/xdef.xdef 的,均为 xdef 模型 - if (XDslConstants.XDSL_SCHEMA_XDEF.equals(getAttributeValue(xdslNs + ":schema"))) { + if (XDslConstants.XDSL_SCHEMA_XDEF.equals(getAttributeValue(xdslKeys.SCHEMA))) { String vfsPath = XmlPsiHelper.getNopVfsPath(this); IXDefinition selfDef; @@ -202,7 +211,7 @@ public class XLangTag extends XmlTagImpl { IXDefNode xdefNode = xdef.getRootNode(); IXDefNode xdslDefNode = XDefPsiHelper.getXDslDef().getRootNode(); - return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefNs, xdslNs); + return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefKeys, xdslKeys); } IXDefinition xdef = parentTag.getXDef(); @@ -211,8 +220,8 @@ public class XLangTag extends XmlTagImpl { } String tagName = getName(); - String xdefNs = parentTag.getXDefNs(); - String xdslNs = parentTag.getXDslNs(); + XDefKeys xdefKeys = parentTag.getXDefKeys(); + XDslKeys xdslKeys = parentTag.getXDslKeys(); IXDefNode parentXDefNode = parentTag.getXDefNode(); IXDefNode parentXDslDefNode = parentTag.getXDslDefNode(); IXDefNode parentSelfDefNode = parentTag.getSelfDefNode(); @@ -235,6 +244,7 @@ public class XLangTag extends XmlTagImpl { } IXDefNode xdslDefNode = parentXDslDefNode != null ? parentXDslDefNode.getChild(tagName) : null; + // TODO x/xdef 名字空间的标签,不取 self def IXDefNode selfDefNode = parentSelfDefNode != null ? parentSelfDefNode.getChild(tagName) : null; IXDefNode xdefNode; @@ -244,7 +254,7 @@ public class XLangTag extends XmlTagImpl { } else { // Note: 如果是 xdef.xdef 中的节点,则其节点 xdef 定义均为 xdef:unknown-tag boolean inXDefXDef = xdef.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // - && !XDefKeys.DEFAULT.NS.equals(xdefNs); // 在单元测试中只能基于内容做判断,而不是 vfs 路径 + && !XDefKeys.DEFAULT.equals(xdefKeys); // 在单元测试中只能基于内容做判断,而不是 vfs 路径 xdefNode = parentXDefNode != null // ? inXDefXDef // @@ -253,7 +263,7 @@ public class XLangTag extends XmlTagImpl { : null; } - return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefNs, xdslNs); + return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefKeys, xdslKeys); } /** @@ -266,19 +276,24 @@ public class XLangTag extends XmlTagImpl { * 注:所有 DSL 模型的节点均与 xdsl.xdef 的节点存在对应 * @param selfDefNode * 若当前标签定义在 xdef 文件中,则需得到其自身的定义节点 - * @param xdefNs - * /nop/schema/xdef.xdef 对应的名字空间。 + * @param xdefKeys + * /nop/schema/xdef.xdef 对应的 {@link XDefKeys}。 * 仅在元模型中设置,如 xmlns:xdef="/nop/schema/xdef.xdef" - * @param xdslNs - * /nop/schema/xdsl.xdef 对应的名字空间。 + * @param xdslKeys + * /nop/schema/xdsl.xdef 对应的 {@link XDslKeys}。 * 在 DSL 模型(含元模型)中均有设置,如 xmlns:x="/nop/schema/xdsl.xdef" */ private record SchemaMeta( // IXDefinition xdef, IXDefNode xdefNode, // IXDefNode xdslDefNode, // IXDefNode selfDefNode, // - String xdefNs, String xdslNs // + XDefKeys xdefKeys, XDslKeys xdslKeys // ) { - public static final SchemaMeta UNKNOWN = new SchemaMeta(null, null, null, null, null, null); + public static final SchemaMeta UNKNOWN = new SchemaMeta(null, + null, + null, + null, + XDefKeys.DEFAULT, + XDslKeys.DEFAULT); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java similarity index 37% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java index bce842816..80ae72f5d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java @@ -1,77 +1,69 @@ package io.nop.idea.plugin.lang.reference; -import java.util.Objects; +import java.util.function.Function; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiElementResolveResult; -import com.intellij.psi.PsiPolyVariantReference; -import com.intellij.psi.ResolveResult; +import com.intellij.psi.PsiFile; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlTag; import io.nop.api.core.util.SourceLocation; import io.nop.idea.plugin.lang.psi.XLangAttribute; import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xdef.IXDefAttribute; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** + * 对属性定义的引用 + *

+ * 指向属性的定义位置 + * * @author flytreeleft * @date 2025-07-10 */ -public class XLangAttributeReference extends XLangReferenceBase implements PsiPolyVariantReference { +public class XLangDefAttrReference extends XLangReferenceBase { - public XLangAttributeReference(XLangAttribute myElement, TextRange myRangeInElement) { + public XLangDefAttrReference(XLangAttribute myElement, TextRange myRangeInElement) { super(myElement, myRangeInElement); } @Override public @Nullable PsiElement resolveInner() { - ResolveResult[] results = multiResolve(false); - - return results.length > 0 ? results[0].getElement() : null; - } - - /** 返回多个引用元素 */ - @Override - public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { XLangAttribute attr = (XLangAttribute) myElement; IXDefAttribute attrDef = attr.getXDefAttr(); if (attrDef == null) { - return ResolveResult.EMPTY_ARRAY; + return null; } - // 若引用属性自身,则直接返回 - if (attr.isSelfDefAttr(attrDef)) { - return new ResolveResult[] { - new PsiElementResolveResult(attr) - }; + // 若为声明属性(定义属性名及其类型),则直接返回 + if (attr.isDeclaredDefAttr(attrDef)) { + return attr; } SourceLocation loc = attrDef.getLocation(); if (loc == null) { - return ResolveResult.EMPTY_ARRAY; + return null; } + Project project = getElement().getProject(); // Note: SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 String path = loc.getPath().replace("classpath:_vfs", ""); - return XmlPsiHelper.findPsiFilesByNopVfsPath(attr, path).stream() // - .map((file) -> { - PsiElement target = XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class); + Function targetResolver = (file) -> { + PsiElement target = XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class); + + if (target == null) { + target = XmlPsiHelper.getPsiElementAt(file, loc, XmlTag.class); - if (target == null) { - target = XmlPsiHelper.getPsiElementAt(file, loc, XmlTag.class); + if (target instanceof XmlTag t) { + target = t.getAttribute(attrDef.getName()); + } + } + return target; + }; - if (target instanceof XmlTag t) { - target = t.getAttribute(attrDef.getName()); - } - } - return target; - }) // - .filter(Objects::nonNull) // - .map(PsiElementResolveResult::new) // - .toArray(ResolveResult[]::new); + return new NopVirtualFile(project, path, targetResolver); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java new file mode 100644 index 000000000..a9aa6c9b8 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java @@ -0,0 +1,100 @@ +package io.nop.idea.plugin.lang.reference; + +import java.util.function.Function; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import io.nop.api.core.beans.DictBean; +import io.nop.api.core.beans.DictOptionBean; +import io.nop.core.dict.DictProvider; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.resource.EnumDictOptionBean; +import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.yaml.psi.YAMLKeyValue; + +/** + * 对字典选项的引用 + * + * @author flytreeleft + * @date 2025-07-12 + */ +public class XLangDictOptionReference extends XLangReferenceBase { + private final String dictName; + private final Object dictOptionValue; + + private DictBean dictBean; + + public XLangDictOptionReference( + PsiElement myElement, TextRange myRangeInElement, // + String dictName + ) { + this(myElement, myRangeInElement, dictName, null); + } + + public XLangDictOptionReference( + PsiElement myElement, TextRange myRangeInElement, // + String dictName, Object dictOptionValue + ) { + super(myElement, myRangeInElement); + this.dictName = dictName; + this.dictOptionValue = dictOptionValue; + } + + public DictBean getDictBean() { + if (dictBean == null) { + dictBean = ProjectEnv.withProject(getElement().getProject(), + () -> DictProvider.instance().getDict(null, dictName, null, null)); + } + return dictBean; + } + + @Override + public @Nullable PsiElement resolveInner() { + DictBean dictBean = getDictBean(); + if (dictBean == null) { + return null; + } + + DictOptionBean dictOpt = dictBean.getOptionByValue(dictOptionValue); + if (dictOpt instanceof EnumDictOptionBean opt) { + return opt.target; + } else { + Function targetResolver = // + (file) -> XmlPsiHelper.findFirstElement(file, (element) -> { + if (element instanceof LeafPsiElement value // + && dictOptionValue.equals(value.getText()) // + ) { + PsiElement parent = // + PsiTreeUtil.getParentOfType(element, YAMLKeyValue.class); + PsiElement key = parent != null ? parent.getFirstChild() : null; + + return key != null && "value".equals(key.getText()); + } + return false; + }); + + Project project = getElement().getProject(); + String path = "/dict/" + dictName + ".dict.yaml"; + + NopVirtualFile target = new NopVirtualFile(project, path, dictOptionValue != null ? targetResolver : null); + + if (target.hasEmptyChildren()) { + String msg = dictOptionValue != null + // + ? NopPluginBundle.message("xlang.annotation.reference.dict-option-not-defined", + dictOptionValue, + path) + : NopPluginBundle.message("xlang.annotation.reference.dict-yaml-not-found", path); + setMessage(msg); + } + return target; + } + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java new file mode 100644 index 000000000..e4f414fe6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java @@ -0,0 +1,39 @@ +package io.nop.idea.plugin.lang.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.messages.NopPluginBundle; +import org.jetbrains.annotations.Nullable; + +/** + * 对父标签上的属性的引用 + * + * @author flytreeleft + * @date 2025-07-13 + */ +public class XLangParentTagAttrReference extends XLangReferenceBase { + private final String attrValue; + + public XLangParentTagAttrReference(XmlElement myElement, TextRange myRangeInElement, String attrValue) { + super(myElement, myRangeInElement); + this.attrValue = attrValue; + } + + @Override + public @Nullable PsiElement resolveInner() { + XLangTag tag = PsiTreeUtil.getParentOfType(myElement, XLangTag.class); + assert tag != null; + + XmlAttribute target = tag.getAttribute(attrValue); + + if (target == null) { + String msg = NopPluginBundle.message("xlang.annotation.reference.parent-tag-attr-not-found", attrValue); + setMessage(msg); + } + return target; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java index e78539281..7e39d34a5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java @@ -1,7 +1,13 @@ package io.nop.idea.plugin.lang.reference; +import com.intellij.psi.PsiReference; + /** * @author flytreeleft * @date 2025-06-23 */ -public interface XLangReference {} +public interface XLangReference extends PsiReference { + + /** 得到告警信息,如,文件不存在、引用目标不存在等 */ + default String getMessage() {return null;} +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java index 318bbec9b..a2ebd91ed 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java @@ -11,12 +11,14 @@ import org.jetbrains.annotations.NotNull; * @author flytreeleft * @date 2025-07-06 */ -public abstract class XLangReferenceBase extends CachingReference { +public abstract class XLangReferenceBase extends CachingReference implements XLangReference { private static final Logger LOG = Logger.getInstance(XLangReferenceBase.class); protected final PsiElement myElement; private TextRange myRangeInElement; + private String message; + /** * 在元素 myElement 可以被拆分为多个相互间有关联的引用时, * 所构造的各个引用均将从属于 myElement,而 @@ -82,6 +84,15 @@ public abstract class XLangReferenceBase extends CachingReference { myRangeInElement = rangeInElement; } + @Override + public String getMessage() { + return this.message; + } + + public void setMessage(String message) { + this.message = message; + } + protected TextRange calculateDefaultRangeInElement() { return getManipulator(myElement).getRangeInElement(myElement); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java new file mode 100644 index 000000000..4e88006c7 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -0,0 +1,189 @@ +package io.nop.idea.plugin.lang.reference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiReference; +import com.intellij.psi.xml.XmlElement; +import io.nop.commons.text.MutableString; +import io.nop.commons.text.tokenizer.TextScanner; +import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.utils.PsiClassHelper; +import io.nop.idea.plugin.vfs.NopVirtualFileReference; +import io.nop.xlang.xdef.XDefConstants; +import io.nop.xlang.xdef.XDefTypeDecl; + +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_DICT; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_ENUM; +import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_ATTR_PREFIX; +import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_PREFIX_OPTIONS; + +/** + * @author flytreeleft + * @date 2025-07-12 + */ +public class XLangReferenceHelper { + + /** + * 根据数据域类型识别引用 + * + * @return 若返回 null,则表示未支持对指定类型的处理 + */ + public static PsiReference[] getReferencesByStdDomain( + XmlElement refElement, String refValue, String stdDomain + ) { + // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, + // 从而精确匹配与引用相关的文本内容 + int textRangeOffset = refElement.getText().indexOf(refValue); + TextRange textRange = new TextRange(0, refValue.length()).shiftRight(textRangeOffset); + + if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain) // + || XDefConstants.STD_DOMAIN_NAME_OR_V_PATH.equals(stdDomain) // + ) { + return getReferencesByVfsPath(refElement, refValue, textRange); + } // + else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { + return getReferencesFromVfsPathCsv(refElement, refValue, textRangeOffset); + } // + else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { + return new PsiReference[] { + new XLangStdDomainXdefRefReference(refElement, textRange, refValue) + }; + } + + return null; + } + + /** 根据属性的类型定义文本识别引用 */ + public static PsiReference[] getReferencesFromDefType( + XmlElement refElement, String refValue, XDefTypeDecl refDefType + ) { + // (!~#)?{stdDomain}(:{options})?(={defaultValue})? + String stdDomain = refDefType.getStdDomain(); + String options = refDefType.getOptions(); + Object defaultValue = refDefType.getDefaultValue(); + List defaultAttrNames = refDefType.getDefaultAttrNames(); + + // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, + // 从而精确匹配与引用相关的文本内容 + int textRangeOffset = refElement.getText().indexOf(refValue); + + int stdDomainIndex = refValue.indexOf(stdDomain); + int optionsIndex = options != null ? refValue.indexOf(XDEF_TYPE_PREFIX_OPTIONS + options) + 1 : -1; + int defaultValueIndex = defaultValue != null ? refValue.indexOf("=" + defaultValue) + 1 : -1; + int defaultAttrNamesIndex = defaultAttrNames != null ? refValue.indexOf('=' + XDEF_TYPE_ATTR_PREFIX) + + 1 + + XDEF_TYPE_ATTR_PREFIX.length() : -1; + + List refs = new ArrayList<>(); + + // 引用数据域的类型定义 + TextRange textRange = new TextRange(0, stdDomain.length()).shiftRight(textRangeOffset + stdDomainIndex); + refs.add(new XLangDictOptionReference(refElement, textRange, "core/std-domain", stdDomain)); + + if (optionsIndex > 0) { + int offset = textRangeOffset + optionsIndex; + textRange = new TextRange(0, options.length()).shiftRight(offset); + + if (STD_DOMAIN_ENUM.equals(stdDomain)) { + // Note: 忽略 enum:a|b|c|d 形式的数据 + if (StringHelper.isValidClassName(options)) { + PsiReference[] ref = PsiClassHelper.createJavaClassReferences(refElement, options, offset); + + Collections.addAll(refs, ref); + } + } // + else if (STD_DOMAIN_DICT.equals(stdDomain)) { + refs.add(new XLangDictOptionReference(refElement, textRange, options)); + } + } + + // 引用字典/枚举值 + if (defaultValueIndex > 0) { + textRange = new TextRange(0, defaultValue.toString().length()).shiftRight(textRangeOffset + + defaultValueIndex); + refs.add(new XLangDictOptionReference(refElement, textRange, options, defaultValue)); + } + + // 引用节点属性 + if (defaultAttrNamesIndex > 0) { + String csv = refValue.substring(defaultAttrNamesIndex); + Map rangeNameMap = extractValuesFromCsv(csv); + + rangeNameMap.forEach((range, name) -> { + TextRange r = range.shiftRight(textRangeOffset + defaultAttrNamesIndex); + + refs.add(new XLangParentTagAttrReference(refElement, r, name)); + }); + } + + return refs.toArray(PsiReference[]::new); + } + + /** 从 csv 文本中识别对 vfs 资源路径的引用 */ + public static PsiReference[] getReferencesFromVfsPathCsv( + XmlElement refElement, String refValue, int textRangeOffset + ) { + Map rangePathMap = extractValuesFromCsv(refValue); + + List list = new ArrayList<>(rangePathMap.size()); + rangePathMap.forEach((textRange, path) -> { + TextRange range = textRange.shiftRight(textRangeOffset); + PsiReference[] refs = getReferencesByVfsPath(refElement, path, range); + + Collections.addAll(list, refs); + }); + + return list.toArray(PsiReference[]::new); + } + + /** 对文本做默认的引用识别 */ + public static PsiReference[] getReferencesFromText(XmlElement refElement, String refValue) { + if (!refValue.endsWith(".xdef")) { + return PsiReference.EMPTY_ARRAY; + } + + // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, + // 从而精确匹配与引用相关的文本内容 + int textRangeOffset = refElement.getText().indexOf(refValue); + TextRange textRange = new TextRange(0, refValue.length()).shiftRight(textRangeOffset); + + return getReferencesByVfsPath(refElement, refValue, textRange); + } + + /** 识别对 vfs 资源路径的引用 */ + public static PsiReference[] getReferencesByVfsPath(XmlElement refElement, String path, TextRange textRange) { + if (!StringHelper.isValidFilePath(path) || path.lastIndexOf('.') <= 0) { + return PsiReference.EMPTY_ARRAY; + } + + return new PsiReference[] { + new NopVirtualFileReference(refElement, textRange, path) + }; + } + + private static Map extractValuesFromCsv(String csv) { + Map rangePathMap = new HashMap<>(); + + TextScanner sc = TextScanner.fromString(null, csv); + + sc.skipBlank(); + while (!sc.isEnd()) { + int offset = sc.pos; + MutableString buf = sc.useBuf(); + sc.nextUntil(s -> s.cur == ',' || StringHelper.isSpace(sc.cur), sc::appendToBuf); + + String value = buf.toString(); + rangePathMap.put(new TextRange(offset, sc.pos), value); + + sc.next(); + sc.skipBlank(); + } + + return rangePathMap; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java new file mode 100644 index 000000000..c940837c3 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java @@ -0,0 +1,89 @@ +package io.nop.idea.plugin.lang.reference; + +import java.util.function.Function; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.xml.XmlAttribute; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import org.jetbrains.annotations.Nullable; + +/** + * {@link io.nop.xlang.xdef.XDefConstants#STD_DOMAIN_XDEF_REF xdef-ref} 类型的值引用 + * + * @author flytreeleft + * @date 2025-07-13 + */ +public class XLangStdDomainXdefRefReference extends XLangReferenceBase { + private final String attrValue; + + public XLangStdDomainXdefRefReference(PsiElement myElement, TextRange myRangeInElement, String attrValue) { + super(myElement, myRangeInElement); + this.attrValue = attrValue; + } + + @Override + public @Nullable PsiElement resolveInner() { + Project project = myElement.getProject(); + // - /nop/schema/xdef.xdef: + // - `` + // - `` + // - /nop/schema/schema/schema-node.xdef: + // `` + + String ref; + String path = null; + PsiElement target; + // 含有后缀的,视为文件引用:相对路径 .. 可能出现在开头,故而,检查最后一个 . 的位置 + if (attrValue.lastIndexOf('.') > 0) { + int hashIndex = attrValue.indexOf('#'); + + path = hashIndex > 0 ? attrValue.substring(0, hashIndex) : attrValue; + ref = hashIndex > 0 ? attrValue.substring(hashIndex + 1) : null; + + path = XmlPsiHelper.getNopVfsAbsolutePath(path, myElement); + + target = new NopVirtualFile(project, path, ref != null ? createTargetResolver(ref) : null); + if (((NopVirtualFile) target).hasEmptyChildren()) { + target = null; + } + } + // 否则,视为名字引用 + else { + ref = attrValue; + // Note: 只能引用当前文件(不一定是 vfs)内的名字 + target = createTargetResolver(ref).apply(myElement.getContainingFile()); + } + + if (target == null) { + String msg = ref == null + ? NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", path) + : path == null + ? NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found", ref) + : NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found-in-path", + ref, + path); + setMessage(msg); + } + return target; + } + + private Function createTargetResolver(String ref) { + return (file) -> (XmlAttribute) XmlPsiHelper.findFirstElement(file, (element) -> { + if (element instanceof XmlAttribute attr) { + String name = attr.getName(); + String value = attr.getValue(); + + // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 + return ("xdef:name".equals(name) // + || "meta:name".equals(name) // + ) && ref.equals(value); + } + return false; + }); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java new file mode 100644 index 000000000..3ebb6ffdc --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java @@ -0,0 +1,66 @@ +package io.nop.idea.plugin.lang.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.xlang.xdef.IXDefNode; +import org.jetbrains.annotations.Nullable; + +/** + * {@link io.nop.xlang.xdsl.XDslKeys#PROTOTYPE x:prototype} 的值引用 + * + * @author flytreeleft + * @date 2025-07-13 + */ +public class XLangXPrototypeReference extends XLangReferenceBase { + private final String attrValue; + + public XLangXPrototypeReference(XmlElement myElement, TextRange myRangeInElement, String attrValue) { + super(myElement, myRangeInElement); + this.attrValue = attrValue; + } + + @Override + public @Nullable PsiElement resolveInner() { + XLangTag tag = PsiTreeUtil.getParentOfType(myElement, XLangTag.class); + assert tag != null; + + XLangTag parentTag = (XLangTag) tag.getParentTag(); + if (parentTag == null) { + String msg = NopPluginBundle.message("xlang.annotation.reference.x-prototype-no-parent"); + setMessage(msg); + + return null; + } + + // 仅从父节点中取引用到的子节点 + // io.nop.xlang.delta.DeltaMerger#mergePrototype + IXDefNode defNode = tag.getXDefNode(); + IXDefNode parentDefNode = parentTag.getXDefNode(); + + String keyAttr = parentDefNode.getXdefKeyAttr(); + if (keyAttr == null) { + keyAttr = defNode.getXdefUniqueAttr(); + } + + XLangTag protoTag = (XLangTag) XmlPsiHelper.getChildTagByAttr(parentTag, keyAttr, attrValue); + if (protoTag == null) { + String msg = keyAttr == null + ? NopPluginBundle.message("xlang.annotation.reference.x-prototype-tag-not-found", + attrValue) + : NopPluginBundle.message("xlang.annotation.reference.x-prototype-attr-not-found", + keyAttr, + attrValue); + setMessage(msg); + + return null; + } + + // 定位到目标属性或标签上 + return keyAttr != null ? protoTag.getAttribute(keyAttr) : protoTag; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java new file mode 100644 index 000000000..259b00f3e --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java @@ -0,0 +1,51 @@ +package io.nop.idea.plugin.lang.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementResolveResult; +import com.intellij.psi.PsiPolyVariantReference; +import com.intellij.psi.ResolveResult; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * {@link io.nop.xlang.xdef.XDefKeys#KEY_ATTR xdef:key-attr} 的值引用 + * + * @author flytreeleft + * @date 2025-07-13 + */ +public class XLangXdefKeyAttrReference extends XLangReferenceBase implements PsiPolyVariantReference { + private final String attrValue; + + public XLangXdefKeyAttrReference(XmlElement myElement, TextRange myRangeInElement, String attrValue) { + super(myElement, myRangeInElement); + this.attrValue = attrValue; + } + + @Override + public @Nullable PsiElement resolveInner() { + ResolveResult[] results = multiResolve(false); + + if (results.length == 0) { + String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-key-attr-not-found", attrValue); + setMessage(msg); + } + + return results.length == 1 ? results[0].getElement() : null; + } + + @Override + public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { + XLangTag tag = PsiTreeUtil.getParentOfType(myElement, XLangTag.class); + assert tag != null; + + return XmlPsiHelper.getAttrsFromChildTag(tag, attrValue).stream() // + .map(PsiElementResolveResult::new) // + .toArray(ResolveResult[]::new); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java index 0a6df6e9f..6929824d6 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -293,7 +293,7 @@ public class ExpressionNode extends RuleSpecNode { // 变量引用:abc if (firstChild instanceof IdentifierNode identifier) { TextRange textRange = identifier.getTextRangeInParent(); - IdentifierReference ref = new IdentifierReference(this, identifier, textRange); + IdentifierReference ref = new IdentifierReference(this, textRange, identifier); return new PsiReference[] { ref }; } @@ -316,7 +316,7 @@ public class ExpressionNode extends RuleSpecNode { TextRange textRange = callee.getTextRangeInParent(); IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); - IdentifierReference ref = new IdentifierReference(this, fn, textRange); + IdentifierReference ref = new IdentifierReference(this, textRange, fn); return new PsiReference[] { ref }; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java index 924ea9ea9..145d27cb9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java @@ -74,7 +74,7 @@ public class ObjectDeclarationNode extends RuleSpecNode { TextRange textRange = propDecl.getTextRangeInParent(); IdentifierNode propNameNode = prop.getPropNameNode(); - IdentifierReference ref = new IdentifierReference(this, propNameNode, textRange); + IdentifierReference ref = new IdentifierReference(this, textRange, propNameNode); result.add(ref); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java index d9d76d63f..add4a5d44 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java @@ -96,7 +96,7 @@ public class QualifiedNameRootNode extends RuleSpecNode { // Note: 取相对于 qnn 的 TextRange 并做偏移 TextRange textRange = identifier.getParent().getTextRangeInParent().shiftRight(offset); - QualifiedNameReference ref = new QualifiedNameReference(this, identifier, textRange, parentReference); + QualifiedNameReference ref = new QualifiedNameReference(this, textRange, identifier, parentReference); result.add(ref); PsiElement sub = qnn.getLastChild(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java index 07d566ee4..00bf95be0 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java @@ -40,7 +40,7 @@ public class TypeNameNodePredefinedNode extends RuleSpecNode { PsiElement typeName = getTypeName(); TextRange textRange = typeName.getTextRangeInParent(); - PredefinedTypeReference ref = new PredefinedTypeReference(this, typeName, textRange); + PredefinedTypeReference ref = new PredefinedTypeReference(this, textRange, typeName); return new PsiReference[] { ref }; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java index 598454d85..10595d7f9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java @@ -18,7 +18,7 @@ import org.jetbrains.annotations.Nullable; public class IdentifierReference extends XLangReferenceBase { private final IdentifierNode identifier; - public IdentifierReference(PsiElement myElement, IdentifierNode identifier, TextRange myRangeInElement) { + public IdentifierReference(PsiElement myElement, TextRange myRangeInElement, IdentifierNode identifier) { super(myElement, myRangeInElement); this.identifier = identifier; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java index 6bad9c688..f12da8675 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java @@ -17,7 +17,7 @@ import org.jetbrains.annotations.Nullable; public class PredefinedTypeReference extends XLangReferenceBase { private final PsiElement typeName; - public PredefinedTypeReference(PsiElement myElement, PsiElement typeName, TextRange myRangeInElement) { + public PredefinedTypeReference(PsiElement myElement, TextRange myRangeInElement, PsiElement typeName) { super(myElement, myRangeInElement); this.typeName = typeName; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java index 5acac3fba..4e811d289 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java @@ -35,8 +35,8 @@ public class QualifiedNameReference extends XLangReferenceBase { private final QualifiedNameReference parentReference; public QualifiedNameReference( - PsiElement myElement, IdentifierNode identifier, TextRange myRangeInElement, - QualifiedNameReference parentReference + PsiElement myElement, TextRange myRangeInElement, // + IdentifierNode identifier, QualifiedNameReference parentReference ) { super(myElement, myRangeInElement); this.identifier = identifier; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceContributor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceContributor.java deleted file mode 100644 index a51d2157f..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceContributor.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.nop.idea.plugin.reference; - -import com.intellij.openapi.fileTypes.FileType; -import com.intellij.patterns.ElementPattern; -import com.intellij.patterns.ObjectPattern; -import com.intellij.patterns.PlatformPatterns; -import com.intellij.patterns.PsiFilePattern; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReferenceContributor; -import com.intellij.psi.PsiReferenceRegistrar; -import io.nop.idea.plugin.lang.XLangFileType; -import org.jetbrains.annotations.NotNull; - -import static com.intellij.psi.PsiReferenceRegistrar.HIGHER_PRIORITY; - -/** - * 对 XLang 中的引用进行识别 - * - * @author flytreeleft - * @date 2025-06-22 - */ -public class XLangReferenceContributor extends PsiReferenceContributor { - - @Override - public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { - ElementPattern pattern = PlatformPatterns.psiElement() - .inFile(withFileType(XLangFileType.class)); - - // Note: 对 XLang 文件的引用解析采用最高优先级,确保在 PsiMultiReference 中将解析到的 XLang 引用作为优先引用 - registrar.registerReferenceProvider(pattern, new XLangReferenceProvider(), HIGHER_PRIORITY); - } - - static PsiFilePattern withFileType(Class type) { - return PlatformPatterns.psiFile().withFileType(new FileTypePattern<>(type)); - } - - static class FileTypePattern extends ObjectPattern> { - - protected FileTypePattern(@NotNull Class aClass) { - super(aClass); - } - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java new file mode 100644 index 000000000..0c43f98ff --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java @@ -0,0 +1,177 @@ +package io.nop.idea.plugin.vfs; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; +import javax.swing.*; + +import com.intellij.codeInsight.navigation.PsiTargetNavigator; +import com.intellij.lang.ASTNode; +import com.intellij.lang.Language; +import com.intellij.navigation.ItemPresentation; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileTypes.PlainTextLanguage; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.impl.PsiElementBase; +import com.intellij.util.IncorrectOperationException; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 为 Nop VFS 资源创建独立的 {@link PsiElement}, + * 从而便于后续为其补充独立的{@link com.intellij.openapi.fileEditor.FileEditor 编辑视图}以支持其 delta 层叠机制 + * + * @author flytreeleft + * @date 2025-07-12 + */ +public class NopVirtualFile extends PsiElementBase implements PsiNamedElement { + private final Project project; + /** vfs 绝对路径 */ + private final String path; + /** 目标元素获取函数 */ + private final Function targetResolver; + + private PsiElement[] children; + + public NopVirtualFile(Project project, String path) { + this(project, path, null); + } + + public NopVirtualFile(Project project, String path, Function targetResolver) { + this.project = project; + this.path = path; + assert path.startsWith("/"); + + this.targetResolver = targetResolver; + } + + @Override + public boolean canNavigate() { + return true; + } + + @Override + public void navigate(boolean requestFocus) { + Editor editor = FileEditorManager.getInstance(getProject()).getSelectedTextEditor(); + if (editor == null) { + return; + } + + // 支持 ctrl + 点击 方式跳转到具体文件 + PsiTargetNavigator navigator = new PsiTargetNavigator<>(() -> Arrays.asList(getChildren())); + navigator.navigate(editor, null); + } + + /** ctrl + 鼠标移动 所显示的元素信息 */ + @Override + public ItemPresentation getPresentation() { + return new ItemPresentation() { + @Override + public @Nullable String getPresentableText() { + return getName(); + } + + @Override + public @Nullable Icon getIcon(boolean unused) { + return null; + } + }; + } + + public boolean hasEmptyChildren() { + return getChildren().length == 0; + } + + @Override + public @NotNull PsiElement @NotNull [] getChildren() { + if (children == null) { + children = XmlPsiHelper.findPsiFilesByNopVfsPath(this, path) + .stream() + .map(file -> targetResolver != null ? targetResolver.apply(file) : file) + .filter(Objects::nonNull) + .toArray(PsiElement[]::new); + } + return children; + } + + @Override + public @NotNull Project getProject() { + return project; + } + + @Override + public String getName() { + return path; + } + + @Override + public PsiElement setName(@NotNull String name) throws IncorrectOperationException { + return null; + } + + @Override + public @NotNull Language getLanguage() { + return PlainTextLanguage.INSTANCE; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public PsiFile getContainingFile() { + return null; + } + + @Override + public String getText() { + return getName(); + } + + @Override + public TextRange getTextRange() { + return TextRange.allOf(getText()); + } + + @Override + public PsiElement getParent() { + return null; + } + + @Override + public int getStartOffsetInParent() { + return 0; + } + + @Override + public int getTextLength() { + return getText().length(); + } + + @Override + public @Nullable PsiElement findElementAt(int offset) { + return null; + } + + @Override + public int getTextOffset() { + return 0; + } + + @Override + public char @NotNull [] textToCharArray() { + return new char[0]; + } + + @Override + public ASTNode getNode() { + return null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java new file mode 100644 index 000000000..10bf883bb --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java @@ -0,0 +1,43 @@ +package io.nop.idea.plugin.vfs; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import io.nop.idea.plugin.lang.reference.XLangReferenceBase; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import org.jetbrains.annotations.Nullable; + +/** + * @author flytreeleft + * @date 2025-07-12 + */ +public class NopVirtualFileReference extends XLangReferenceBase { + private final String path; + + /** + * @param path + * 可以为相对路径,最终通过 myElement 确定其绝对路径 + */ + public NopVirtualFileReference( + PsiElement myElement, TextRange myRangeInElement, // + String path + ) { + super(myElement, myRangeInElement); + this.path = path; + } + + @Override + public @Nullable PsiElement resolveInner() { + Project project = myElement.getProject(); + String absPath = XmlPsiHelper.getNopVfsAbsolutePath(path, myElement); + + NopVirtualFile target = new NopVirtualFile(project, absPath); + + if (target.hasEmptyChildren()) { + String msg = NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", path); + setMessage(msg); + } + return target; + } +} diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 082d84079..1d6e530ab 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -94,8 +94,6 @@ - - diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties index d1d5be302..b0052f29e 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties @@ -7,11 +7,12 @@ xlang.annotation.value.not-allow-empty=Tag ''{0}'' value not allow empty xlang.annotation.tag.not-allow-value=Tag ''{0}'' not allow value xlang.annotation.tag.not-allow-child=Tag ''{0}'' not allow child xlang.annotation.reference.vfs-file-not-found=The referenced vfs file ''{0}'' doesn''t exist +xlang.annotation.reference.dict-yaml-not-found=The dict yaml file ''{0}'' doesn''t exist +xlang.annotation.reference.dict-option-not-defined=The dict option ''{0}'' isn''t defined in ''{1}'' xlang.annotation.reference.xdef-ref-not-found=No xdef defined node named ''{0}'' exists xlang.annotation.reference.xdef-ref-not-found-in-path=No xdef defined node named ''{0}'' in ''{1}'' -xlang.annotation.reference.xdef-unique-attr-not-found=No attribute named ''{0}'' in current node +xlang.annotation.reference.parent-tag-attr-not-found=No attribute named ''{0}'' in current node xlang.annotation.reference.xdef-key-attr-not-found=No child node which has attribute named ''{0}'' exists -xlang.annotation.reference.default-value-ref-attr-not-found=No attribute named ''{0}'' in current node xlang.annotation.reference.x-prototype-no-parent=Only child node can define ''x:prototype'' xlang.annotation.reference.x-prototype-tag-not-found=No sibling node named ''{0}'' exists xlang.annotation.reference.x-prototype-attr-not-found=No sibling node which has attribute ''{0}={1}'' exists diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties index 636386ae2..4c23a1985 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties @@ -7,10 +7,11 @@ xlang.annotation.value.not-allow-empty=\u6807\u7B7E ''{0}'' \u7684\u503C\u4E0D\u xlang.annotation.tag.not-allow-value=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5185\u5BB9\u8282\u70B9 xlang.annotation.tag.not-allow-child=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5B50\u8282\u70B9 xlang.annotation.reference.vfs-file-not-found=\u5F15\u7528\u7684 vfs \u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 +xlang.annotation.reference.dict-yaml-not-found=\u5B57\u5178\u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 +xlang.annotation.reference.dict-option-not-defined=\u9009\u9879 ''{0}'' \u672A\u5728\u5B57\u5178\u6587\u4EF6 ''{1}'' \u4E2D\u5B9A\u4E49 xlang.annotation.reference.xdef-ref-not-found=\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 xlang.annotation.reference.xdef-ref-not-found-in-path=\u5728 ''{1}'' \u4E2D\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 -xlang.annotation.reference.xdef-unique-attr-not-found=\u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 -xlang.annotation.reference.default-value-ref-attr-not-found=\u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 +xlang.annotation.reference.parent-tag-attr-not-found=\u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 xlang.annotation.reference.xdef-key-attr-not-found=\u5728\u5B50\u8282\u70B9\u4E2D\u672A\u5B9A\u4E49\u5C5E\u6027 ''{0}'' xlang.annotation.reference.x-prototype-no-parent=\u53EA\u6709\u5728\u5B50\u8282\u70B9\u4E0A\u624D\u80FD\u5B9A\u4E49\u5C5E\u6027 ''x:prototype'' xlang.annotation.reference.x-prototype-tag-not-found=\u4E0D\u5B58\u5728\u6807\u7B7E\u540D\u4E3A ''{0}'' \u7684\u5144\u5F1F\u8282\u70B9 diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 4a25995a8..45ae9b6de 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -8,6 +8,7 @@ import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlTag; import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; /** * @author flytreeleft @@ -345,24 +346,25 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { } public void testAttributeTypeReferences() { - // 声明属性将 引用 属性的类型定义 - // TODO 暂时无法通过分析 class 字节码得到可注册的数据域 +// // 声明属性将 引用 属性的类型定义 +// // TODO 暂时无法通过分析 class 字节码得到可注册的数据域 // // - #getName 返回引用值 -// doTest(""" -// -// -// -// """, "/dict/test/doc/child-type.dict.yaml#leaf"); +// assertReference(""" +// +// +// +// """, "/dict/test/doc/child-type.dict.yaml#leaf"); // // - #getName 返回字面量值 -// doTest(""" -// -// -// -// """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); +// assertReference(""" +// +// +// +// """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + // - 引用字典中定义的数据域 assertReference(""" Date: Mon, 14 Jul 2025 17:28:24 +0800 Subject: [PATCH 53/82] =?UTF-8?q?nop-xlang:=20=E8=A1=A5=E5=85=85=E8=BE=85?= =?UTF-8?q?=E5=8A=A9=E6=96=B9=E6=B3=95=20XDefKeys#of(String)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java b/nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java index ca85809ff..399c9f610 100644 --- a/nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java +++ b/nop-xlang/src/main/java/io/nop/xlang/xdef/XDefKeys.java @@ -22,6 +22,10 @@ public class XDefKeys implements Serializable { public static XDefKeys of(XNode node) { String ns = node.getXmlnsForUrl(XDslConstants.XDSL_SCHEMA_XDEF); + return of(ns); + } + + public static XDefKeys of(String ns) { if (ns == null || ns.equals(DEFAULT.NS)) return DEFAULT; return new XDefKeys(ns); -- Gitee From 9cab1480ed88182736b147f88b060220b37b54fe Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Mon, 14 Jul 2025 17:29:25 +0800 Subject: [PATCH 54/82] =?UTF-8?q?nop-idea-pugin:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=8E=20XLang=20=E5=B1=9E=E6=80=A7=E5=80=BC=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/annotation/XLangAnnotator.java | 47 +++-- .../plugin/lang/psi/XLangAttributeValue.java | 10 +- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 34 +++- .../reference/XLangDictOptionReference.java | 2 +- .../XLangParentTagAttrReference.java | 2 +- .../plugin/lang/reference/XLangReference.java | 4 +- .../lang/reference/XLangReferenceBase.java | 10 +- .../lang/reference/XLangReferenceHelper.java | 11 +- .../XLangStdDomainXdefRefReference.java | 2 +- .../reference/XLangXPrototypeReference.java | 4 +- .../reference/XLangXdefKeyAttrReference.java | 2 +- .../reference/XLangNotFoundReference.java | 2 +- .../nop/idea/plugin/utils/XDefPsiHelper.java | 2 +- .../nop/idea/plugin/vfs/NopVirtualFile.java | 16 +- .../plugin/vfs/NopVirtualFileReference.java | 4 +- .../idea/plugin/lang/TestXLangReferences.java | 190 ++++++++++-------- .../test/resources/_vfs/test/doc/example.xdef | 3 +- 17 files changed, 211 insertions(+), 134 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java index 6887a73e5..a0f0ebdee 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java @@ -31,11 +31,11 @@ import io.nop.idea.plugin.lang.reference.XLangReference; import io.nop.idea.plugin.messages.NopPluginBundle; import io.nop.idea.plugin.reference.XLangVfsFileReference; import io.nop.idea.plugin.reference.XLangElementReference; -import io.nop.idea.plugin.reference.XLangNotFoundReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.utils.XmlTagInfo; +import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xdef.IStdDomainHandler; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefNode; @@ -70,26 +70,7 @@ public class XLangAnnotator implements Annotator { return; } - for (PsiReference reference : element.getReferences()) { - if (reference instanceof XLangVfsFileReference // - || reference instanceof XLangElementReference // - ) { - holder.newSilentAnnotation(HighlightSeverity.INFORMATION) - .range(reference.getAbsoluteRange()) - .textAttributes(DefaultLanguageHighlighterColors.DOC_COMMENT_TAG_VALUE) - .create(); - } else if (reference instanceof XLangReference ref) { - ref.resolve(); // 确保已检查异常状态 - String msg = ref.getMessage(); - - if (msg != null) { - holder.newAnnotation(HighlightSeverity.ERROR, msg) - .range(ref.getAbsoluteRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); - } - } - } + checkReferences(element, holder); if (element instanceof XmlTag) { XmlTag tag = (XmlTag) element; @@ -127,6 +108,30 @@ public class XLangAnnotator implements Annotator { return XDefPsiHelper.getTagInfo(element); } + private void checkReferences(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + for (PsiReference reference : element.getReferences()) { + if (!(reference instanceof XLangReference ref)) { + continue; + } + + PsiElement target = ref.resolve(); + if (target instanceof NopVirtualFile vfs && vfs.forFileChildren()) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(reference.getAbsoluteRange()) + .textAttributes(DefaultLanguageHighlighterColors.DOC_COMMENT_TAG_VALUE) + .create(); + } + + String msg = ref.getUnresolvedMessage(); + if (target == null && msg != null) { + holder.newAnnotation(HighlightSeverity.ERROR, msg) + .range(ref.getAbsoluteRange()) + .highlightType(ProblemHighlightType.ERROR) + .create(); + } + } + } + private boolean checkTag(XmlTagInfo tagInfo, AnnotationHolder holder) { XmlTag tag = tagInfo.getTag(); String tagNameStr = tag.getName(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index 494354ac2..2afd09db7 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -5,10 +5,10 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.XmlAttributeValueImpl; import io.nop.commons.util.StringHelper; -import io.nop.idea.plugin.lang.reference.XLangReferenceHelper; -import io.nop.idea.plugin.lang.reference.XLangXdefKeyAttrReference; import io.nop.idea.plugin.lang.reference.XLangParentTagAttrReference; +import io.nop.idea.plugin.lang.reference.XLangReferenceHelper; import io.nop.idea.plugin.lang.reference.XLangXPrototypeReference; +import io.nop.idea.plugin.lang.reference.XLangXdefKeyAttrReference; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.XDefKeys; import io.nop.xlang.xdef.XDefTypeDecl; @@ -39,9 +39,11 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { return PsiReference.EMPTY_ARRAY; } + String attrName = attr.getName(); IXDefAttribute attrDef = attr.getXDefAttr(); + // 对于未定义属性,不做引用识别 if (attrDef == null) { - return XLangReferenceHelper.getReferencesFromText(this, attrValue); + return PsiReference.EMPTY_ARRAY; } XDefTypeDecl attrDefType = attrDef.getType(); @@ -52,6 +54,7 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { // 根据属性的类型,对属性值做文件/名字引用 PsiReference[] refs = XLangReferenceHelper.getReferencesByStdDomain(this, + attrName, attrValue, attrDefType.getStdDomain()); if (refs != null) { @@ -62,7 +65,6 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { XDslKeys xdslKeys = tag.getXDslKeys(); XDefKeys xdefKeys = tag.getXDefKeys(); - String attrName = attr.getName(); // Note: XmlAttributeValue 的文本范围是包含引号的 TextRange attrValueTextRange = getValueTextRange().shiftLeft(getStartOffset()); if (xdslKeys.PROTOTYPE.equals(attrName)) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 71b20ede1..87fc441dc 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -14,9 +14,11 @@ import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefinition; +import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefKeys; import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.impl.XDefAttribute; +import io.nop.xlang.xdef.parse.XDefTypeDeclParser; import io.nop.xlang.xdsl.XDslConstants; import io.nop.xlang.xdsl.XDslKeys; import org.jetbrains.annotations.NotNull; @@ -30,6 +32,9 @@ import org.jetbrains.annotations.NotNull; * @date 2025-07-09 */ public class XLangTag extends XmlTagImpl { + private static final XDefTypeDecl STD_DOMAIN_XDEF_REF = new XDefTypeDeclParser().parseFromText(null, + XDefConstants.STD_DOMAIN_XDEF_REF); + private SchemaMeta schemaMeta; @Override @@ -115,6 +120,14 @@ public class XLangTag extends XmlTagImpl { /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ private static IXDefAttribute getXDefNodeAttr(IXDefNode xdefNode, String attrName) { + if (attrName.startsWith("xmlns:")) { + XDefAttribute at = new XDefAttribute(); + at.setName(attrName); + at.setType(STD_DOMAIN_XDEF_REF); + + return at; + } + if (xdefNode == null) { return null; } @@ -183,12 +196,11 @@ public class XLangTag extends XmlTagImpl { return SchemaMeta.UNKNOWN; } + // Note: 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 IXDefinition xdef = XDefPsiHelper.loadSchema(schemaUrl); - if (xdef == null) { - return SchemaMeta.UNKNOWN; - } - XDefKeys xdefKeys = xdef.getDefKeys(); + String xdefNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDEF); + XDefKeys xdefKeys = XDefKeys.of(xdefNs); String xdslNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDSL); XDslKeys xdslKeys = XDslKeys.of(xdslNs); @@ -208,16 +220,14 @@ public class XLangTag extends XmlTagImpl { selfDefNode = selfDef != null ? selfDef.getRootNode() : null; } - IXDefNode xdefNode = xdef.getRootNode(); + IXDefNode xdefNode = xdef != null ? xdef.getRootNode() : null; IXDefNode xdslDefNode = XDefPsiHelper.getXDslDef().getRootNode(); return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefKeys, xdslKeys); } + // Note: 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 IXDefinition xdef = parentTag.getXDef(); - if (xdef == null) { - return SchemaMeta.UNKNOWN; - } String tagName = getName(); XDefKeys xdefKeys = parentTag.getXDefKeys(); @@ -230,8 +240,9 @@ public class XLangTag extends XmlTagImpl { // xlib.xdef 中的 source 标签设置为 xml 类型,是因为在获取 XplLib 模型的时候会根据 xlib.xdef 来解析, // 但此时这个 source 段无法自动进行编译,必须结合它的 outputMode 和 attrs 配置等才能决定。 // 因此,将其子节点同样视为 xpl 节点处理 - || ((xdef.resourcePath().equals(XDslConstants.XDSL_SCHEMA_XLIB) // - || xdef.resourcePath().endsWith("_vfs" + XDslConstants.XDSL_SCHEMA_XLIB) // + || (xdef != null // + && (xdef.resourcePath().equals(XDslConstants.XDSL_SCHEMA_XLIB) // + || xdef.resourcePath().endsWith("_vfs" + XDslConstants.XDSL_SCHEMA_XLIB) // ) // && "xml".equals(XDefPsiHelper.getDefNodeType(parentXDefNode)) // && "source".equals(parentTag.getName()) // @@ -253,7 +264,8 @@ public class XLangTag extends XmlTagImpl { xdefNode = xdslDefNode; } else { // Note: 如果是 xdef.xdef 中的节点,则其节点 xdef 定义均为 xdef:unknown-tag - boolean inXDefXDef = xdef.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // + boolean inXDefXDef = xdef != null // + && xdef.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // && !XDefKeys.DEFAULT.equals(xdefKeys); // 在单元测试中只能基于内容做判断,而不是 vfs 路径 xdefNode = parentXDefNode != null // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java index a9aa6c9b8..d0bd4a3ab 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java @@ -92,7 +92,7 @@ public class XLangDictOptionReference extends XLangReferenceBase { dictOptionValue, path) : NopPluginBundle.message("xlang.annotation.reference.dict-yaml-not-found", path); - setMessage(msg); + setUnresolvedMessage(msg); } return target; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java index e4f414fe6..ab5bc0abc 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java @@ -32,7 +32,7 @@ public class XLangParentTagAttrReference extends XLangReferenceBase { if (target == null) { String msg = NopPluginBundle.message("xlang.annotation.reference.parent-tag-attr-not-found", attrValue); - setMessage(msg); + setUnresolvedMessage(msg); } return target; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java index 7e39d34a5..05346dd71 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java @@ -8,6 +8,6 @@ import com.intellij.psi.PsiReference; */ public interface XLangReference extends PsiReference { - /** 得到告警信息,如,文件不存在、引用目标不存在等 */ - default String getMessage() {return null;} + /** {@link #resolve()} 结果为 null 时的消息,如,文件不存在、引用目标不存在等 */ + default String getUnresolvedMessage() {return null;} } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java index a2ebd91ed..c9ec93802 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java @@ -17,7 +17,7 @@ public abstract class XLangReferenceBase extends CachingReference implements XLa protected final PsiElement myElement; private TextRange myRangeInElement; - private String message; + private String unresolvedMessage; /** * 在元素 myElement 可以被拆分为多个相互间有关联的引用时, @@ -85,12 +85,12 @@ public abstract class XLangReferenceBase extends CachingReference implements XLa } @Override - public String getMessage() { - return this.message; + public String getUnresolvedMessage() { + return this.unresolvedMessage; } - public void setMessage(String message) { - this.message = message; + public void setUnresolvedMessage(String unresolvedMessage) { + this.unresolvedMessage = unresolvedMessage; } protected TextRange calculateDefaultRangeInElement() { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java index 4e88006c7..66f0bd9b9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -9,13 +9,16 @@ import java.util.Map; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiReference; import com.intellij.psi.xml.XmlElement; +import io.nop.api.core.util.SourceLocation; import io.nop.commons.text.MutableString; import io.nop.commons.text.tokenizer.TextScanner; import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.utils.PsiClassHelper; +import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFileReference; import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; +import io.nop.xlang.xdsl.XDslParseHelper; import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_DICT; import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_ENUM; @@ -34,7 +37,7 @@ public class XLangReferenceHelper { * @return 若返回 null,则表示未支持对指定类型的处理 */ public static PsiReference[] getReferencesByStdDomain( - XmlElement refElement, String refValue, String stdDomain + XmlElement refElement, String attrName, String refValue, String stdDomain ) { // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, // 从而精确匹配与引用相关的文本内容 @@ -53,6 +56,12 @@ public class XLangReferenceHelper { return new PsiReference[] { new XLangStdDomainXdefRefReference(refElement, textRange, refValue) }; + } // + else if (XDefConstants.STD_DOMAIN_DEF_TYPE.equals(stdDomain)) { + SourceLocation loc = XmlPsiHelper.getLocation(refElement); + XDefTypeDecl refDefType = XDslParseHelper.parseDefType(loc, attrName, refValue); + + return getReferencesFromDefType(refElement, refValue, refDefType); } return null; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java index c940837c3..ecc8413f1 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java @@ -67,7 +67,7 @@ public class XLangStdDomainXdefRefReference extends XLangReferenceBase { : NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found-in-path", ref, path); - setMessage(msg); + setUnresolvedMessage(msg); } return target; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java index 3ebb6ffdc..166c19c94 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java @@ -32,7 +32,7 @@ public class XLangXPrototypeReference extends XLangReferenceBase { XLangTag parentTag = (XLangTag) tag.getParentTag(); if (parentTag == null) { String msg = NopPluginBundle.message("xlang.annotation.reference.x-prototype-no-parent"); - setMessage(msg); + setUnresolvedMessage(msg); return null; } @@ -55,7 +55,7 @@ public class XLangXPrototypeReference extends XLangReferenceBase { : NopPluginBundle.message("xlang.annotation.reference.x-prototype-attr-not-found", keyAttr, attrValue); - setMessage(msg); + setUnresolvedMessage(msg); return null; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java index 259b00f3e..ec23fdd98 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java @@ -33,7 +33,7 @@ public class XLangXdefKeyAttrReference extends XLangReferenceBase implements Psi if (results.length == 0) { String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-key-attr-not-found", attrValue); - setMessage(msg); + setUnresolvedMessage(msg); } return results.length == 1 ? results[0].getElement() : null; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java index 612255a9b..c60203735 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangNotFoundReference.java @@ -23,7 +23,7 @@ public class XLangNotFoundReference extends PsiReferenceBase impleme this.message = message; } - public String getMessage() { + public String getUnresolvedMessage() { return message; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index d7a55e713..7b9edece6 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -111,7 +111,7 @@ public class XDefPsiHelper { public static IXDefinition loadSchema(PsiFile file) { String content = file.getText(); // Note: 解析过程中,会检查路径的有效性,需保证以 / 开头,并添加 .xdef 后缀 - IResource resource = new InMemoryTextResource('/' + file.getVirtualFile().getName() + ".xdef", content); + IResource resource = new InMemoryTextResource("/" + file.getText().hashCode() + ".xdef", content); try { return new XDefinitionParser().parseFromResource(resource); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java index 0c43f98ff..7e73764d5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java @@ -51,6 +51,11 @@ public class NopVirtualFile extends PsiElementBase implements PsiNamedElement { this.targetResolver = targetResolver; } + @Override + public String toString() { + return getClass().getSimpleName() + ':' + getPath(); + } + @Override public boolean canNavigate() { return true; @@ -88,6 +93,11 @@ public class NopVirtualFile extends PsiElementBase implements PsiNamedElement { return getChildren().length == 0; } + /** {@link #getChildren()} 是否为 {@link PsiFile} */ + public boolean forFileChildren() { + return targetResolver == null; + } + @Override public @NotNull PsiElement @NotNull [] getChildren() { if (children == null) { @@ -105,9 +115,13 @@ public class NopVirtualFile extends PsiElementBase implements PsiNamedElement { return project; } + public String getPath() { + return path; + } + @Override public String getName() { - return path; + return getPath(); } @Override diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java index 10bf883bb..6e30ea59b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java @@ -36,7 +36,9 @@ public class NopVirtualFileReference extends XLangReferenceBase { if (target.hasEmptyChildren()) { String msg = NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", path); - setMessage(msg); + setUnresolvedMessage(msg); + + return null; } return target; } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 45ae9b6de..29e2a498f 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -1,7 +1,12 @@ package io.nop.idea.plugin.lang; +import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiPlainText; import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.impl.source.xml.SchemaPrefix; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; @@ -9,6 +14,7 @@ import com.intellij.psi.xml.XmlTag; import io.nop.idea.plugin.BaseXLangPluginTestCase; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFile; +import org.jetbrains.annotations.NotNull; /** * @author flytreeleft @@ -67,6 +73,8 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(readVfsResource("/test/doc/example.xdef").replace("me=\"string\""), "child#name=string"); + assertReference(readVfsResource("/test/doc/example.xdef").replace("xpl:dump", "xpl:dump"), + "/nop/schema/xpl.xdef?xdef:define#xpl:dump=boolean"); assertReference(""" + + mp="true"/> + + + """, "/nop/schema/xpl.xdef?xdef:define#xpl:dump=boolean"); assertReference(""" """, "/test/reference/default.xform"); @@ -195,6 +213,15 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { """, "/test/reference/b.xmeta"); // 对 xdef-ref 类型属性的引用 + // - xmlns:xxx 默认为 xdef-ref 类型 + assertReference(""" + + """, "/nop/schema/xdsl.xdef"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xmlns:xdef=\"/nop/schema/xdef.xdef\"", + "xmlns:xdef=\"/nop/schema/xdef.xdef\""), + "/nop/schema/xdef.xdef"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xmlns:x=\"x\"", "xmlns:x=\"x\""), + null); // - 在 *.xdef 中引用内部名字 assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", "meta:ref=\"XDefNode\""), @@ -202,10 +229,10 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), "xdef:unknown-tag#xdef:name=DslNode"); -// // - 引用文件的相对路径出现在开头 -// doTest(readVfsResource("/nop/schema/xui/simple-component.xdef").replace("xdef:ref=\"../xui/import.xdef\"", -// "xdef:ref=\"../xui/import.xdef\""), -// "/nop/schema/xui/import.xdef"); + // - 引用文件的相对路径出现在开头:单元测试中暂时无法查找 vfs 相对路径 +// assertReference(readVfsResource("/nop/schema/xui/simple-component.xdef").replace( +// "xdef:ref=\"../xui/import.xdef\"", +// "xdef:ref=\"../xui/import.xdef\""), "/nop/schema/xui/import.xdef"); assertReference(""" - """, "xdef:define#xdef:name=FilterCondition"); + """, "/test/reference/test-filter.xdef?xdef:define#xdef:name=FilterCondition"); // - 外部文件中的引用节点不存在 assertReference(""" """, null); - // 未知 schema 导致引用无法识别,但支持对 *.xdef 的引用识别 + // x:schema 指定的 *.xdef 不存在,使得 DSL 的元模型未定义,导致模型属性未知,其引用将无法识别 + // - *.xdef 不存在 assertReference(""" """, null); + // - 属性未定义,引用无法识别 assertReference(""" - - """, "/nop/schema/xdsl.xdef"); + + + + """, null); // // TODO 对 xpl 属性的文件引用 -// doTest(""" -// -// """, "/test/reference/a.xlib"); -// doTest(""" -// -// """, "/test/reference/a.xlib"); +// assertReference(""" +// +// """, "/test/reference/a.xlib"); +// assertReference(""" +// +// """, "/test/reference/a.xlib"); } - public void testAttributeTypeReferences() { -// // 声明属性将 引用 属性的类型定义 + public void testAttributeValueTypeReferences() { + // 声明属性将 引用 属性的类型定义 + assertReference(""" + + + + """, "/dict/core/std-domain.dict.yaml#v-path"); // // TODO 暂时无法通过分析 class 字节码得到可注册的数据域 -// // - #getName 返回引用值 -// assertReference(""" -// -// -// -// """, "/dict/test/doc/child-type.dict.yaml#leaf"); -// // - #getName 返回字面量值 // assertReference(""" // """, "/dict/test/doc/child-type.dict.yaml"); + assertReference(""" + + + + """, null); assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace( "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // @@ -395,6 +431,13 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { """, "/dict/test/doc/child-type.dict.yaml#leaf"); + assertReference(""" + + + + """, null); assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace( "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // @@ -406,21 +449,21 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { x:schema="/nop/schema/xdef.xdef" > - + """, "import#name=var-name"); assertReference(""" - + """, "var#type=!string"); assertReference(""" - + """, null); } @@ -478,60 +521,49 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { } assertNotNull(target); - // Note: 可能不是 vfs 文件 + assertEquals(expected, toString(target)); + } + + private String toString(@NotNull PsiElement target) { + // Note: 可能不是 vfs 文件中的元素 String vfsPath = XmlPsiHelper.getNopVfsPath(target); - if (target instanceof XmlAttribute attr) { + if (target instanceof XmlTag tag) { + return tag.getName(); + } // + else if (target instanceof SchemaPrefix ns) { + return ns.getName(); + } // + else if (target instanceof NopVirtualFile vfs) { + PsiElement child = vfs.getFirstChild(); + + return child instanceof PsiFile ? vfs.getPath() : toString(child); + } // + else if (target instanceof XmlAttribute attr) { XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); - assertEquals(expected, - (vfsPath != null ? vfsPath + '?' : "") - + tag.getName() - + '#' - + attr.getName() - + '=' - + attr.getValue()); - } else if (target instanceof SchemaPrefix ns) { - assertEquals(expected, ns.getName()); - } else if (target instanceof NopVirtualFile vfs) { - assertEquals(expected, vfs.getName()); + return (vfsPath != null ? vfsPath + '?' : "") + + tag.getName() + + '#' + + attr.getName() + + '=' + + attr.getValue(); + } // + else if (target instanceof PsiClass cls) { + return cls.getQualifiedName(); + } // + else if (target instanceof PsiField field) { + return field.getContainingClass().getQualifiedName() + "#" + field.getName(); + } // + else if (target instanceof PsiPlainText txt) { + return vfsPath + ":" + txt.getTextOffset(); + } // + else if (target instanceof LeafPsiElement leaf) { + return vfsPath + "#" + leaf.getText(); + } // + else { + fail("Unknown target " + target.getClass()); } - -// if (ref instanceof XLangVfsFileReference) { -// // Note: 可能不是 vfs 文件 -// String vfsPath = XmlPsiHelper.getNopVfsPath(target); -// String anchor = target instanceof XmlAttribute attr ? attr.getValue() : null; -// -// assertEquals(expected, (vfsPath != null ? vfsPath : "") + (anchor != null ? "#" + anchor : "")); -// } // -// else if (ref instanceof XLangElementReference || ref instanceof XLangXDefReference) { -// if (target instanceof XmlTag tag) { -// assertEquals(expected, tag.getName()); -// } // -// else if (target instanceof XmlAttribute attr) { -// XmlTag tag = PsiTreeUtil.getParentOfType(attr, XmlTag.class); -// -// assertEquals(expected, tag.getName() + "#" + attr.getName() + "=" + attr.getValue()); -// } // -// else if (target instanceof PsiClass cls) { -// assertEquals(expected, cls.getQualifiedName()); -// } // -// else if (target instanceof PsiField field) { -// assertEquals(expected, field.getContainingClass().getQualifiedName() + "#" + field.getName()); -// } // -// else if (target instanceof PsiPlainText txt) { -// String vfsPath = XmlPsiHelper.getNopVfsPath(target); -// -// assertEquals(expected, vfsPath + ":" + txt.getTextOffset()); -// } // -// else if (target instanceof LeafPsiElement leaf) { -// String vfsPath = XmlPsiHelper.getNopVfsPath(target); -// -// assertEquals(expected, vfsPath + "#" + leaf.getText()); -// } // -// else { -// fail("Unknown target " + target.getClass()); -// } -// } + return null; } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index c3c0f4b4d..64e3a07f9 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -2,7 +2,8 @@ - -- Gitee From 5ec31f02edb754e48d035309037aa67ba97df8e2 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 15 Jul 2025 15:35:22 +0800 Subject: [PATCH 55/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E7=9A=84=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E7=9A=84=E6=96=B9=E5=BC=8F=E5=A4=84=E7=90=86?= =?UTF-8?q?=20DSL=20=E6=A8=A1=E5=9E=8B=E3=80=81=E5=85=83=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=B1=9E=E6=80=A7=E5=AE=9A=E4=B9=89=E5=8F=8A?= =?UTF-8?q?=E5=85=B6=E5=80=BC=E5=86=85=E5=BC=95=E7=94=A8=E7=9A=84=E8=AF=86?= =?UTF-8?q?=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../idea/plugin/lang/psi/XLangAttribute.java | 32 +-- .../plugin/lang/psi/XLangAttributeValue.java | 29 +-- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 245 +++++++++++------- .../lang/reference/XLangDefAttrReference.java | 18 +- .../lang/reference/XLangReferenceHelper.java | 126 ++++++--- .../XLangStdDomainGenericTypeReference.java | 38 +++ .../reference/XLangXPrototypeReference.java | 4 +- .../nop/idea/plugin/utils/XDefPsiHelper.java | 3 +- .../idea/plugin/lang/TestXLangReferences.java | 128 +++++++-- .../test/resources/_vfs/test/doc/example.xdef | 2 +- 10 files changed, 403 insertions(+), 222 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java index aa391164a..f27541eb0 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -44,48 +44,26 @@ public class XLangAttribute extends XmlAttributeImpl { return ref0 != null ? new PsiReference[] { ref0, ref1 } : new PsiReference[] { ref1 }; } - /** 获取当前属性的 xdef 定义 */ - public IXDefAttribute getXDefAttr() { + /** 获取当前属性在元模型中的定义 */ + public IXDefAttribute getDefAttr() { XLangTag tag = (XLangTag) getParent(); if (tag == null) { return null; } - // - 对于声明属性(定义属性名及其类型),从其自身(*.xdef)中取其定义 - // - 对于赋值属性(为具体属性赋予相应类型的值),从其 x:schema 中取其定义 - // - 对于名字空间对应 xdsl.xdef 和 xdef.xdef 的属性,则分别从这两个元模型中取属性定义 - IXDefAttribute attrDef; - String ns = getNamespacePrefix(); String attrName = getName(); - boolean hasXDefNs = !ns.isEmpty() && ns.equals(tag.getXDefKeys().NS); boolean hasXDslNs = !ns.isEmpty() && ns.equals(tag.getXDslKeys().NS); + IXDefAttribute attrDef; // 取 xdsl.xdef 中声明的属性 if (hasXDslNs) { attrDef = tag.getXDslDefNodeAttr(attrName); } // - else if (tag.isInXDef()) { - // 取 xdef.xdef 中声明的属性 - if (hasXDefNs) { - attrDef = tag.getXDefNodeAttr(attrName); - } - // 取自身声明的属性 - else { - attrDef = tag.getSelfDefNodeAttr(attrName); - } - } else { - attrDef = tag.getXDefNodeAttr(attrName); + else { + attrDef = tag.getSchemaDefNodeAttr(attrName); } return attrDef; } - - /** 是否为声明属性(定义属性名及其类型) */ - public boolean isDeclaredDefAttr(IXDefAttribute attr) { - String attrName = getName(); - XLangTag tag = (XLangTag) getParent(); - - return tag != null && attr == tag.getSelfDefNodeAttr(attrName); - } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index 2afd09db7..4547ed6d2 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -40,31 +40,17 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { } String attrName = attr.getName(); - IXDefAttribute attrDef = attr.getXDefAttr(); + IXDefAttribute attrDef = attr.getDefAttr(); // 对于未定义属性,不做引用识别 if (attrDef == null) { return PsiReference.EMPTY_ARRAY; } - XDefTypeDecl attrDefType = attrDef.getType(); - // 对于声明属性(定义属性名及其类型),仅对其类型的定义(涉及枚举和字典)做引用识别 - if (attr.isDeclaredDefAttr(attrDef)) { - return XLangReferenceHelper.getReferencesFromDefType(this, attrValue, attrDefType); - } - - // 根据属性的类型,对属性值做文件/名字引用 - PsiReference[] refs = XLangReferenceHelper.getReferencesByStdDomain(this, - attrName, - attrValue, - attrDefType.getStdDomain()); - if (refs != null) { - return refs; - } - XLangTag tag = (XLangTag) attr.getParent(); XDslKeys xdslKeys = tag.getXDslKeys(); XDefKeys xdefKeys = tag.getXDefKeys(); + // 根据属性名,从属性值中查找引用 // Note: XmlAttributeValue 的文本范围是包含引号的 TextRange attrValueTextRange = getValueTextRange().shiftLeft(getStartOffset()); if (xdslKeys.PROTOTYPE.equals(attrName)) { @@ -77,12 +63,21 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { new XLangXdefKeyAttrReference(this, attrValueTextRange, attrValue) }; } // - else if (xdefKeys.UNIQUE_ATTR.equals(attrName)) { + else if (xdefKeys.UNIQUE_ATTR.equals(attrName) // + || xdefKeys.ORDER_ATTR.equals(attrName) // + ) { return new PsiReference[] { new XLangParentTagAttrReference(this, attrValueTextRange, attrValue) }; } + // 根据属性定义类型,从属性值中查找引用 + XDefTypeDecl attrDefType = attrDef.getType(); + PsiReference[] refs = XLangReferenceHelper.getReferencesByAttrDefType(this, attrValue, attrDefType); + if (refs != null) { + return refs; + } + // TODO 其他引用识别 // // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 87fc441dc..fc3359dcb 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -6,6 +6,7 @@ import com.intellij.openapi.project.Project; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.XmlTagImpl; +import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.core.lang.xml.XNode; import io.nop.idea.plugin.resource.ProjectEnv; @@ -21,6 +22,7 @@ import io.nop.xlang.xdef.impl.XDefAttribute; import io.nop.xlang.xdef.parse.XDefTypeDeclParser; import io.nop.xlang.xdsl.XDslConstants; import io.nop.xlang.xdsl.XDslKeys; +import io.nop.xlang.xpl.xlib.XlibConstants; import org.jetbrains.annotations.NotNull; /** @@ -65,33 +67,55 @@ public class XLangTag extends XmlTagImpl { return super.getReferences(hints); } - /** 当前标签是否在 xdef 文件中 */ - public boolean isInXDef() { - return getSelfDefNode() != null; - } - - /** @see SchemaMeta#xdef */ - private IXDefinition getXDef() { - return getSchemaMeta().xdef; + /** + * 获取当前标签所在的元模型 + * + * @see SchemaMeta#schemaDef + */ + private IXDefinition getSchemaDef() { + return getSchemaMeta().schemaDef; } - /** @see SchemaMeta#xdefNode */ - public IXDefNode getXDefNode() { - return getSchemaMeta().xdefNode; + /** + * 获取当前标签在{@link #getSchemaDef() 元模型}中对应的节点 + * + * @see SchemaMeta#schemaDefNode + */ + public IXDefNode getSchemaDefNode() { + return getSchemaMeta().schemaDefNode; } - public IXDefAttribute getXDefNodeAttr(String attrName) { - // Note: xdef.xdef 的属性在固定的名字空间 xdef 中声明 - attrName = changeNamespace(attrName, getXDefKeys().NS, XDefKeys.DEFAULT.NS); + /** + * 获取当前标签上指定属性在元模型中的定义 + *

+ * 在元元模型 xdef.xdef 中,名字空间 meta 的属性均由同名的以 + * xdef 为名字空间的属性定义,而以 xdef + * 为名字空间的属性,则均由 meta:unknown-attr 定义。 + * 即,二者形成交叉定义 + */ + public IXDefAttribute getSchemaDefNodeAttr(String attrName) { + // 在元元模型中,以 xdef 为名字空间的属性,需迫使其以 meta:unknown-attr 作为其属性定义 + if (isInXDefXDef() && attrName.startsWith(XDefKeys.DEFAULT.NS + ':')) { + attrName = "*"; + } + // xdef.xdef 的属性在固定的名字空间 x 中声明 + else { + attrName = changeNamespace(attrName, getXDefKeys().NS, XDefKeys.DEFAULT.NS); + } - return getXDefNodeAttr(getXDefNode(), attrName); + return getXDefNodeAttr(getSchemaDefNode(), attrName); } - /** @see SchemaMeta#xdslDefNode */ + /** + * 获取当前标签在 xdsl.xdef 中对应的节点 + * + * @see SchemaMeta#xdslDefNode + */ public IXDefNode getXDslDefNode() { return getSchemaMeta().xdslDefNode; } + /** 获取当前标签上指定属性在 xdsl.xdef 中的定义 */ public IXDefAttribute getXDslDefNodeAttr(String attrName) { // Note: xdsl.xdef 的属性在固定的名字空间 x 中声明 attrName = changeNamespace(attrName, getXDslKeys().NS, XDslKeys.DEFAULT.NS); @@ -99,6 +123,11 @@ public class XLangTag extends XmlTagImpl { return getXDefNodeAttr(getXDslDefNode(), attrName); } + /** @see SchemaMeta#selfDef */ + public IXDefinition getSelfDef() { + return getSchemaMeta().selfDef; + } + /** @see SchemaMeta#selfDefNode */ public IXDefNode getSelfDefNode() { return getSchemaMeta().selfDefNode; @@ -118,8 +147,30 @@ public class XLangTag extends XmlTagImpl { return getSchemaMeta().xdslKeys; } + /** 当前标签是否在元元模型 xdef.xdef 中 */ + private boolean isInXDefXDef() { + IXDefinition def = getSelfDef(); + XDefKeys xdefKeys = getXDefKeys(); + + // Note: 在单元测试中只能基于内容做判断,而不是 vfs 路径 + return def != null // + && def.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // + && !XDefKeys.DEFAULT.equals(xdefKeys); + } + + /** 当前标签是否在 DSL 元模型 xdsl.xdef 中 */ + private boolean isInXDslXDef() { + IXDefinition def = getSelfDef(); + XDslKeys xdslKeys = getXDslKeys(); + + // Note: 在单元测试中只能基于内容做判断,而不是 vfs 路径 + return def != null // + && def.getXdefCheckNs().contains(XDslKeys.DEFAULT.NS) // + && !XDslKeys.DEFAULT.equals(xdslKeys); + } + /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ - private static IXDefAttribute getXDefNodeAttr(IXDefNode xdefNode, String attrName) { + private IXDefAttribute getXDefNodeAttr(IXDefNode xdefNode, String attrName) { if (attrName.startsWith("xmlns:")) { XDefAttribute at = new XDefAttribute(); at.setName(attrName); @@ -137,8 +188,7 @@ public class XLangTag extends XmlTagImpl { return attr; } - // Note: 在普通 *.xdef 的 IXDefNode 中, - // 对 xdef:unknown-attr 只记录了类型,并没有 IXDefAttribute 实体, + // Note: 在 IXDefNode 中,对 xdef:unknown-attr 只记录了类型,并没有 IXDefAttribute 实体, // 其处理逻辑见 XDefinitionParser#parseNode XDefTypeDecl xdefUnknownAttrType = xdefNode.getXdefUnknownAttr(); if (xdefUnknownAttrType != null) { @@ -149,10 +199,22 @@ public class XLangTag extends XmlTagImpl { } }; - at.setName(XDefKeys.DEFAULT.UNKNOWN_ATTR); + at.setName(getXDefKeys().UNKNOWN_ATTR); at.setType(xdefUnknownAttrType); + // Note: 在需要时,通过节点位置再定位具体的属性位置 - at.setLocation(xdefNode.getLocation()); + SourceLocation loc = null; + // 在存在节点继承的情况下,选择最上层定义的同类型的 unknown-attr 属性 + IXDefNode refNode = xdefNode; + while (refNode != null) { + if (refNode.getXdefUnknownAttr() != xdefUnknownAttrType) { + break; + } + + loc = refNode.getLocation(); + refNode = refNode.getRefNode(); + } + at.setLocation(loc); return at; } @@ -188,106 +250,104 @@ public class XLangTag extends XmlTagImpl { private SchemaMeta createSchemaMeta() { XLangTag parentTag = (XLangTag) getParentTag(); - - // 当前为根标签 if (parentTag == null) { - String schemaUrl = XDefPsiHelper.getSchemaPath(this); - if (schemaUrl == null) { - return SchemaMeta.UNKNOWN; - } - - // Note: 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 - IXDefinition xdef = XDefPsiHelper.loadSchema(schemaUrl); - - String xdefNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDEF); - XDefKeys xdefKeys = XDefKeys.of(xdefNs); - String xdslNs = XmlPsiHelper.getXmlnsForUrl(this, XDslConstants.XDSL_SCHEMA_XDSL); - XDslKeys xdslKeys = XDslKeys.of(xdslNs); - - IXDefNode selfDefNode = null; - // x:schema 为 /nop/schema/xdef.xdef 的,均为 xdef 模型 - if (XDslConstants.XDSL_SCHEMA_XDEF.equals(getAttributeValue(xdslKeys.SCHEMA))) { - String vfsPath = XmlPsiHelper.getNopVfsPath(this); - - IXDefinition selfDef; - if (vfsPath != null) { - selfDef = XDefPsiHelper.loadSchema(vfsPath); - } else { - // 适配单元测试环境:待测试资源可能不是标准的 vfs 资源 - selfDef = XDefPsiHelper.loadSchema(getContainingFile()); - } - - selfDefNode = selfDef != null ? selfDef.getRootNode() : null; - } - - IXDefNode xdefNode = xdef != null ? xdef.getRootNode() : null; - IXDefNode xdslDefNode = XDefPsiHelper.getXDslDef().getRootNode(); - - return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefKeys, xdslKeys); + return createSchemaMetaForRootTag(this); } + String tagNs = getNamespacePrefix(); + String tagName = getName(); + // Note: 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 - IXDefinition xdef = parentTag.getXDef(); + IXDefinition schemaDef = parentTag.getSchemaDef(); + IXDefinition selfDef = parentTag.getSelfDef(); - String tagName = getName(); XDefKeys xdefKeys = parentTag.getXDefKeys(); XDslKeys xdslKeys = parentTag.getXDslKeys(); - IXDefNode parentXDefNode = parentTag.getXDefNode(); + IXDefNode parentSchemaDefNode = parentTag.getSchemaDefNode(); IXDefNode parentXDslDefNode = parentTag.getXDslDefNode(); IXDefNode parentSelfDefNode = parentTag.getSelfDefNode(); - boolean xpl = XDefPsiHelper.isXplDefNode(parentXDefNode) // + boolean xpl = XDefPsiHelper.isXplDefNode(parentSchemaDefNode) // // xlib.xdef 中的 source 标签设置为 xml 类型,是因为在获取 XplLib 模型的时候会根据 xlib.xdef 来解析, // 但此时这个 source 段无法自动进行编译,必须结合它的 outputMode 和 attrs 配置等才能决定。 // 因此,将其子节点同样视为 xpl 节点处理 - || (xdef != null // - && (xdef.resourcePath().equals(XDslConstants.XDSL_SCHEMA_XLIB) // - || xdef.resourcePath().endsWith("_vfs" + XDslConstants.XDSL_SCHEMA_XLIB) // + || (schemaDef != null // + && (schemaDef.resourcePath().equals(XDslConstants.XDSL_SCHEMA_XLIB) // + || schemaDef.resourcePath().endsWith("_vfs" + XDslConstants.XDSL_SCHEMA_XLIB) // ) // - && "xml".equals(XDefPsiHelper.getDefNodeType(parentXDefNode)) // - && "source".equals(parentTag.getName()) // + && XDefConstants.STD_DOMAIN_XML.equals(XDefPsiHelper.getDefNodeType(parentSchemaDefNode)) // + && XlibConstants.SOURCE_NAME.equals(parentTag.getName()) // ); if (xpl) { - // 对于 Xpl 节点,始终采用 xpl.xdef 作为其子节点的元模型 - xdef = XDefPsiHelper.getXplDef(); // Xpl 子节点均为 xdef:unknown-tag - parentXDefNode = xdef.getRootNode().getXdefUnknownTag(); + parentSchemaDefNode = XDefPsiHelper.getXplDef().getRootNode().getXdefUnknownTag(); } IXDefNode xdslDefNode = parentXDslDefNode != null ? parentXDslDefNode.getChild(tagName) : null; - // TODO x/xdef 名字空间的标签,不取 self def IXDefNode selfDefNode = parentSelfDefNode != null ? parentSelfDefNode.getChild(tagName) : null; - IXDefNode xdefNode; - if (tagName.startsWith(XDslKeys.DEFAULT.X_NS_PREFIX)) { - xdef = XDefPsiHelper.getXDslDef(); - xdefNode = xdslDefNode; - } else { - // Note: 如果是 xdef.xdef 中的节点,则其节点 xdef 定义均为 xdef:unknown-tag - boolean inXDefXDef = xdef != null // - && xdef.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // - && !XDefKeys.DEFAULT.equals(xdefKeys); // 在单元测试中只能基于内容做判断,而不是 vfs 路径 - - xdefNode = parentXDefNode != null // - ? inXDefXDef // - ? parentXDefNode.getXdefUnknownTag() // - : parentXDefNode.getChild(tagName) // - : null; + IXDefNode schemaDefNode = null; + if (tagNs.equals(XDslKeys.DEFAULT.NS) && !parentTag.isInXDslXDef()) { + schemaDefNode = xdslDefNode; + } // + else if (parentSchemaDefNode != null) { + // Note: 如果是 xdef.xdef 中的节点,则其节点定义均为 xdef:unknown-tag + boolean inXDefXDef = parentTag.isInXDefXDef(); + + schemaDefNode = inXDefXDef // + ? parentSchemaDefNode.getXdefUnknownTag() // + : parentSchemaDefNode.getChild(tagName); } - return new SchemaMeta(xdef, xdefNode, xdslDefNode, selfDefNode, xdefKeys, xdslKeys); + return new SchemaMeta(schemaDef, schemaDefNode, xdslDefNode, selfDef, selfDefNode, xdefKeys, xdslKeys); + } + + private static SchemaMeta createSchemaMetaForRootTag(XLangTag rootTag) { + String schemaUrl = XDefPsiHelper.getSchemaPath(rootTag); + if (schemaUrl == null) { + return SchemaMeta.UNKNOWN; + } + + // Note: 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 + IXDefinition schemaDef = XDefPsiHelper.loadSchema(schemaUrl); + + String xdefNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDEF); + String xdslNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); + XDefKeys xdefKeys = XDefKeys.of(xdefNs); + XDslKeys xdslKeys = XDslKeys.of(xdslNs); + + IXDefinition selfDef = null; + // x:schema 为 /nop/schema/xdef.xdef 时,其自身也为元模型 + if (XDslConstants.XDSL_SCHEMA_XDEF.equals(schemaUrl)) { + String vfsPath = XmlPsiHelper.getNopVfsPath(rootTag); + + if (vfsPath != null) { + selfDef = XDefPsiHelper.loadSchema(vfsPath); + } else { + // 适配单元测试环境:待测试资源可能不是标准的 vfs 资源 + selfDef = XDefPsiHelper.loadSchema(rootTag.getContainingFile()); + } + } + + IXDefNode schemaDefNode = schemaDef != null ? schemaDef.getRootNode() : null; + IXDefNode xdslDefNode = XDefPsiHelper.getXDslDef().getRootNode(); + IXDefNode selfDefNode = selfDef != null ? selfDef.getRootNode() : null; + + return new SchemaMeta(schemaDef, schemaDefNode, xdslDefNode, selfDef, selfDefNode, xdefKeys, xdslKeys); } /** - * @param xdef + * @param schemaDef * 当前标签所在的元模型(在 *.xdef 中定义) - * @param xdefNode - * 当前标签在 {@link #xdef} 中所对应的节点 + * @param schemaDefNode + * 当前标签在 {@link #schemaDef} 中所对应的节点 * @param xdslDefNode - * 当前标签在 xdsl.xdef 中所对应的节点。 + * 当前标签在 xdsl 模型(xdsl.xdef)中所对应的节点。 * 注:所有 DSL 模型的节点均与 xdsl.xdef 的节点存在对应 + * @param selfDef + * 在当前标签定义在 *.xdef 文件中时,需记录该元模型 * @param selfDefNode - * 若当前标签定义在 xdef 文件中,则需得到其自身的定义节点 + * 当前标签定义在 {@link #selfDefNode} 中所对应的节点 * @param xdefKeys * /nop/schema/xdef.xdef 对应的 {@link XDefKeys}。 * 仅在元模型中设置,如 xmlns:xdef="/nop/schema/xdef.xdef" @@ -296,15 +356,16 @@ public class XLangTag extends XmlTagImpl { * 在 DSL 模型(含元模型)中均有设置,如 xmlns:x="/nop/schema/xdsl.xdef" */ private record SchemaMeta( // - IXDefinition xdef, IXDefNode xdefNode, // + IXDefinition schemaDef, IXDefNode schemaDefNode, // IXDefNode xdslDefNode, // - IXDefNode selfDefNode, // + IXDefinition selfDef, IXDefNode selfDefNode, // XDefKeys xdefKeys, XDslKeys xdslKeys // ) { public static final SchemaMeta UNKNOWN = new SchemaMeta(null, null, null, null, + null, XDefKeys.DEFAULT, XDslKeys.DEFAULT); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java index 80ae72f5d..6cffc885a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java @@ -16,9 +16,7 @@ import io.nop.xlang.xdef.IXDefAttribute; import org.jetbrains.annotations.Nullable; /** - * 对属性定义的引用 - *

- * 指向属性的定义位置 + * 对属性定义的引用:指向属性的定义位置 * * @author flytreeleft * @date 2025-07-10 @@ -32,22 +30,17 @@ public class XLangDefAttrReference extends XLangReferenceBase { @Override public @Nullable PsiElement resolveInner() { XLangAttribute attr = (XLangAttribute) myElement; - IXDefAttribute attrDef = attr.getXDefAttr(); + IXDefAttribute attrDef = attr.getDefAttr(); if (attrDef == null) { return null; } - // 若为声明属性(定义属性名及其类型),则直接返回 - if (attr.isDeclaredDefAttr(attrDef)) { - return attr; - } - SourceLocation loc = attrDef.getLocation(); if (loc == null) { return null; } - Project project = getElement().getProject(); + Project project = myElement.getProject(); // Note: SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 String path = loc.getPath().replace("classpath:_vfs", ""); @@ -57,8 +50,9 @@ public class XLangDefAttrReference extends XLangReferenceBase { if (target == null) { target = XmlPsiHelper.getPsiElementAt(file, loc, XmlTag.class); - if (target instanceof XmlTag t) { - target = t.getAttribute(attrDef.getName()); + if (target instanceof XmlTag tag) { + // Note: 在交叉定义时,属性定义中的属性名字与当前属性名字是不相同的 + target = tag.getAttribute(attrDef.getName()); } } return target; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java index 66f0bd9b9..571bd9dbe 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -5,23 +5,33 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiReference; import com.intellij.psi.xml.XmlElement; -import io.nop.api.core.util.SourceLocation; import io.nop.commons.text.MutableString; import io.nop.commons.text.tokenizer.TextScanner; import io.nop.commons.util.StringHelper; +import io.nop.core.type.IGenericType; import io.nop.idea.plugin.utils.PsiClassHelper; -import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFileReference; -import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdsl.XDslParseHelper; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_CLASS_NAME; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_CLASS_NAME_SET; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_DEF_TYPE; import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_DICT; import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_ENUM; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_GENERIC_TYPE; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_GENERIC_TYPE_LIST; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_NAME_OR_V_PATH; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_PACKAGE_NAME; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_V_PATH; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_V_PATH_LIST; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_XDEF_ATTR; +import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_XDEF_REF; import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_ATTR_PREFIX; import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_PREFIX_OPTIONS; @@ -32,61 +42,76 @@ import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_PREFIX_OPTIONS; public class XLangReferenceHelper { /** - * 根据数据域类型识别引用 + * 根据{@link XDefTypeDecl 属性定义类型}识别引用 * * @return 若返回 null,则表示未支持对指定类型的处理 */ - public static PsiReference[] getReferencesByStdDomain( - XmlElement refElement, String attrName, String refValue, String stdDomain + public static PsiReference[] getReferencesByAttrDefType( + XmlElement refElement, String refValue, XDefTypeDecl attrDefType ) { // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, // 从而精确匹配与引用相关的文本内容 int textRangeOffset = refElement.getText().indexOf(refValue); TextRange textRange = new TextRange(0, refValue.length()).shiftRight(textRangeOffset); - if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain) // - || XDefConstants.STD_DOMAIN_NAME_OR_V_PATH.equals(stdDomain) // - ) { - return getReferencesByVfsPath(refElement, refValue, textRange); - } // - else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { - return getReferencesFromVfsPathCsv(refElement, refValue, textRangeOffset); - } // - else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { - return new PsiReference[] { - new XLangStdDomainXdefRefReference(refElement, textRange, refValue) - }; - } // - else if (XDefConstants.STD_DOMAIN_DEF_TYPE.equals(stdDomain)) { - SourceLocation loc = XmlPsiHelper.getLocation(refElement); - XDefTypeDecl refDefType = XDslParseHelper.parseDefType(loc, attrName, refValue); - - return getReferencesFromDefType(refElement, refValue, refDefType); - } - - return null; + String stdDomain = attrDefType.getStdDomain(); + return switch (stdDomain) { + case STD_DOMAIN_XDEF_REF -> // + new PsiReference[] { + new XLangStdDomainXdefRefReference(refElement, textRange, refValue) + }; + case STD_DOMAIN_V_PATH, STD_DOMAIN_NAME_OR_V_PATH -> // + getReferencesByVfsPath(refElement, refValue, textRange); + case STD_DOMAIN_V_PATH_LIST -> // + getReferencesFromVfsPathCsv(refElement, refValue, textRangeOffset); + case STD_DOMAIN_GENERIC_TYPE, STD_DOMAIN_GENERIC_TYPE_LIST -> // + getReferencesFromGenericTypeCsv(refElement, refValue, textRangeOffset); + case STD_DOMAIN_CLASS_NAME, STD_DOMAIN_CLASS_NAME_SET, STD_DOMAIN_PACKAGE_NAME -> // + PsiClassHelper.createJavaClassReferences(refElement, refValue, textRangeOffset); + case STD_DOMAIN_DICT, STD_DOMAIN_ENUM -> // + new PsiReference[] { + new XLangDictOptionReference(refElement, textRange, attrDefType.getOptions(), refValue) + }; + case STD_DOMAIN_XDEF_ATTR, STD_DOMAIN_DEF_TYPE -> // + getReferencesFromDefType(refElement, refValue, refValue); + default -> null; + }; } - /** 根据属性的类型定义文本识别引用 */ + /** 根据属性的类型定义识别引用 */ public static PsiReference[] getReferencesFromDefType( - XmlElement refElement, String refValue, XDefTypeDecl refDefType + XmlElement refElement, String refValue, String refDefTypeText ) { + XDefTypeDecl refDefType; + try { + refDefType = XDslParseHelper.parseDefType(null, null, refDefTypeText); + } catch (Exception ignore) { + return PsiReference.EMPTY_ARRAY; + } + // (!~#)?{stdDomain}(:{options})?(={defaultValue})? String stdDomain = refDefType.getStdDomain(); String options = refDefType.getOptions(); - Object defaultValue = refDefType.getDefaultValue(); + String defaultValue = Objects.toString(refDefType.getDefaultValue(), null); List defaultAttrNames = refDefType.getDefaultAttrNames(); // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, // 从而精确匹配与引用相关的文本内容 int textRangeOffset = refElement.getText().indexOf(refValue); - int stdDomainIndex = refValue.indexOf(stdDomain); - int optionsIndex = options != null ? refValue.indexOf(XDEF_TYPE_PREFIX_OPTIONS + options) + 1 : -1; - int defaultValueIndex = defaultValue != null ? refValue.indexOf("=" + defaultValue) + 1 : -1; - int defaultAttrNamesIndex = defaultAttrNames != null ? refValue.indexOf('=' + XDEF_TYPE_ATTR_PREFIX) - + 1 - + XDEF_TYPE_ATTR_PREFIX.length() : -1; + int indexOffset = 0; + int stdDomainIndex = refValue.indexOf(stdDomain, indexOffset); + + indexOffset = stdDomainIndex + stdDomain.length(); + int optionsIndex = options != null ? refValue.indexOf(XDEF_TYPE_PREFIX_OPTIONS + options, indexOffset) + 1 : -1; + + indexOffset = optionsIndex + (options != null ? options.length() : 1); + int defaultValueIndex = defaultValue != null ? refValue.indexOf('=' + defaultValue, indexOffset) + 1 : -1; + int defaultAttrNamesIndex = defaultAttrNames != null // + ? refValue.indexOf('=' + XDEF_TYPE_ATTR_PREFIX, indexOffset) + + 1 + + XDEF_TYPE_ATTR_PREFIX.length() // + : -1; List refs = new ArrayList<>(); @@ -113,8 +138,8 @@ public class XLangReferenceHelper { // 引用字典/枚举值 if (defaultValueIndex > 0) { - textRange = new TextRange(0, defaultValue.toString().length()).shiftRight(textRangeOffset - + defaultValueIndex); + textRange = new TextRange(0, defaultValue.length()).shiftRight(textRangeOffset + defaultValueIndex); + refs.add(new XLangDictOptionReference(refElement, textRange, options, defaultValue)); } @@ -137,12 +162,12 @@ public class XLangReferenceHelper { public static PsiReference[] getReferencesFromVfsPathCsv( XmlElement refElement, String refValue, int textRangeOffset ) { - Map rangePathMap = extractValuesFromCsv(refValue); + Map rangeMap = extractValuesFromCsv(refValue); - List list = new ArrayList<>(rangePathMap.size()); - rangePathMap.forEach((textRange, path) -> { + List list = new ArrayList<>(rangeMap.size()); + rangeMap.forEach((textRange, value) -> { TextRange range = textRange.shiftRight(textRangeOffset); - PsiReference[] refs = getReferencesByVfsPath(refElement, path, range); + PsiReference[] refs = getReferencesByVfsPath(refElement, value, range); Collections.addAll(list, refs); }); @@ -175,6 +200,23 @@ public class XLangReferenceHelper { }; } + /** 从 csv 文本中识别对 {@link IGenericType} 的引用 */ + public static PsiReference[] getReferencesFromGenericTypeCsv( + XmlElement refElement, String refValue, int textRangeOffset + ) { + Map rangeMap = extractValuesFromCsv(refValue); + + List list = new ArrayList<>(rangeMap.size()); + rangeMap.forEach((textRange, value) -> { + TextRange range = textRange.shiftRight(textRangeOffset); + PsiReference ref = new XLangStdDomainGenericTypeReference(refElement, range, value); + + list.add(ref); + }); + + return list.toArray(PsiReference[]::new); + } + private static Map extractValuesFromCsv(String csv) { Map rangePathMap = new HashMap<>(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.java new file mode 100644 index 000000000..3b3efd297 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.java @@ -0,0 +1,38 @@ +package io.nop.idea.plugin.lang.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import io.nop.core.type.IGenericType; +import io.nop.core.type.parse.GenericTypeParser; +import io.nop.idea.plugin.utils.PsiClassHelper; +import org.jetbrains.annotations.Nullable; + +/** + * {@link io.nop.xlang.xdef.XDefConstants#STD_DOMAIN_GENERIC_TYPE generic-type} 类型的值引用 + * + * @author flytreeleft + * @date 2025-07-15 + */ +public class XLangStdDomainGenericTypeReference extends XLangReferenceBase { + private final String attrValue; + + public XLangStdDomainGenericTypeReference(PsiElement myElement, TextRange myRangeInElement, String attrValue) { + super(myElement, myRangeInElement); + this.attrValue = attrValue; + } + + @Override + public @Nullable PsiElement resolveInner() { + IGenericType type = null; + try { + type = new GenericTypeParser().parseFromText(null, attrValue); + } catch (Exception ignore) { + } + + if (type == null) { + return null; + } + + return PsiClassHelper.findClass(myElement, type.getClassName()); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java index 166c19c94..1ab2c2ce9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java @@ -39,8 +39,8 @@ public class XLangXPrototypeReference extends XLangReferenceBase { // 仅从父节点中取引用到的子节点 // io.nop.xlang.delta.DeltaMerger#mergePrototype - IXDefNode defNode = tag.getXDefNode(); - IXDefNode parentDefNode = parentTag.getXDefNode(); + IXDefNode defNode = tag.getSchemaDefNode(); + IXDefNode parentDefNode = parentTag.getSchemaDefNode(); String keyAttr = parentDefNode.getXdefKeyAttr(); if (keyAttr == null) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 7b9edece6..3b1cb2927 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -67,8 +67,7 @@ public class XDefPsiHelper { String ns = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); if (ns == null) { - String prefix = XDslKeys.DEFAULT.X_NS_PREFIX; - ns = prefix.substring(0, prefix.length() - 1); + ns = XDslKeys.DEFAULT.NS; } return ns; } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 29e2a498f..665e436e7 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -4,6 +4,7 @@ import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiField; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiPackage; import com.intellij.psi.PsiPlainText; import com.intellij.psi.PsiReference; import com.intellij.psi.impl.source.tree.LeafPsiElement; @@ -36,46 +37,87 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { } public void testAttributeReferences() { - // xdef.xdef 中的引用识别 + // xdef.xdef 属性的交叉定义识别 + // - 名字空间引用其自身 assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", "meta:unique-attr=\"name\""), "meta"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), - "/nop/schema/xdef.xdef?meta:define#xdef:unique-attr=xml-name"); - - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), - "xdef:prop#name=!xml-name"); assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("ef:name=\"!var-name\""), "xdef"); + // - 以 meta 为名字空间的属性(含 meta:unknown-attr)由对应的 xdef:xxx 定义 + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unknown-attr=\"!xdef-attr\"", + "meta:unknown-attr=\"!xdef-attr\""), + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:bean-tag-prop=\"tagName\"", + "meta:bean-tag-prop=\"tagName\""), + "/nop/schema/xdef.xdef?meta:define#xdef:bean-tag-prop=prop-name"); + // - 全部以 xdef 为名字空间的属性均由 meta:unknown-attr 定义 + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:unknown-attr=\"def-type\"", + "xdef:unknown-attr=\"def-type\""), + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:unique-attr=\"xml-name\"", + "xdef:unique-attr=\"xml-name\""), + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", + "xdef:ref=\"xdef-ref\""), + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); + // - xdef 或 meta 名字空间的节点属性,也满足以上规则 + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", + "f=\"XDefNode\"/>"), + "/nop/schema/xdef.xdef?meta:define#xdef:ref=xdef-ref"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", + "f=\"XDefNode\"/>"), + "/nop/schema/xdef.xdef?meta:define#xdef:ref=xdef-ref"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unknown-attr=\"string\"", + "meta:unknown-attr=\"string\""), + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("ame=\"!var-name\""), - "xdef:define#xdef:name=!var-name"); + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"XDefNode\""), "/nop/schema/xdef.xdef?meta:define#xdef:name=var-name"); - // xdsl.xdef 中的引用识别 + // xdsl.xdef 中的属性定义识别 + // - 交叉识别 assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=", "xdsl:schema="), "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:schema=v-path"); + // - 普通定义 assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", "x:schema=\"v-path\""), - "xdef:unknown-tag#x:schema=v-path"); + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:allow-multiple=\"true\"", "xdef:allow-multiple=\"true\""), "/nop/schema/xdef.xdef?meta:define#xdef:allow-multiple=boolean"); assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("x:key-attr=\"xml-name\"", "x:key-attr=\"xml-name\""), - "xdef:unknown-tag#x:key-attr=xml-name"); - - // + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("", + "lue=\"xpl-node\"/>"), + "/nop/schema/xdef.xdef?meta:define#xdef:value=def-type"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("", + "ernal=\"true\"/>"), + "/nop/schema/xdef.xdef?meta:define#xdef:internal=boolean"); + + // 普通 xdef 的属性定义识别 assertReference(readVfsResource("/test/doc/example.xdef").replace("me=\"string\""), - "child#name=string"); + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); + assertReference(readVfsResource("/test/doc/example.xdef").replace("x:dump", "x:dump"), + "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:dump=boolean"); assertReference(readVfsResource("/test/doc/example.xdef").replace("xpl:dump", "xpl:dump"), "/nop/schema/xpl.xdef?xdef:define#xpl:dump=boolean"); + assertReference(""" + + ract="true"/> + + """, "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:abstract=boolean"); assertReference(""" """, null); + // generic-type、class-name、package-name 类型的值引用 + assertReference(""" + + + + """, "java.lang.String"); + assertReference(""" + + + + """, "io.nop.xui.initialize.VueNodeStdDomainHandler"); + assertReference(""" + + """, "io.nop.xlang.xdef"); + + // dict/enum 类型的值引用 + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:default-override=\"append\"", + "xdef:default-override=\"append\""), // + "io.nop.xlang.xdef.XDefOverride#APPEND"); + assertReference(""" + + + + """, "/dict/test/doc/child-type.dict.yaml#leaf"); + // x:schema 指定的 *.xdef 不存在,使得 DSL 的元模型未定义,导致模型属性未知,其引用将无法识别 // - *.xdef 不存在 assertReference(""" @@ -376,8 +452,13 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { // """, "/test/reference/a.xlib"); } - public void testAttributeValueTypeReferences() { - // 声明属性将 引用 属性的类型定义 + public void testAttributeValueDefTypeReferences() { + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace( + "xdef:default-override=\"enum:io.nop.xlang.xdef.XDefOverride\"", + "xdef:default-override=\"enum:io.nop.xlang.xdef.XDefOverride\""), + "io.nop.xlang.xdef.XDefOverride"); + + // 引用字典中定义的数据域 assertReference(""" """, "/dict/core/std-domain.dict.yaml#v-path"); -// // TODO 暂时无法通过分析 class 字节码得到可注册的数据域 -// assertReference(""" -// -// -// -// """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); - - // - 引用字典中定义的数据域 assertReference(""" -- Gitee From c4ec6625a09608024fea4d1cdffc014bdb5bea44 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 15 Jul 2025 21:01:19 +0800 Subject: [PATCH 56/82] =?UTF-8?q?nop-idea-pugin:=20=E8=B0=83=E6=95=B4=20XL?= =?UTF-8?q?ang=20=E7=9A=84=E8=AF=AD=E6=B3=95=E6=A0=A1=E9=AA=8C=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=B9=B6=E8=A1=A5=E5=85=85=E5=AF=B9=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{annotation => lang}/XLangAnnotator.java | 271 ++++++++---------- .../idea/plugin/lang/psi/XLangAttribute.java | 6 +- .../plugin/lang/psi/XLangAttributeValue.java | 56 +++- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 32 ++- .../reference/XLangDictOptionReference.java | 47 ++- .../XLangParentTagAttrReference.java | 4 +- .../lang/reference/XLangReferenceHelper.java | 57 +++- .../reference/XLangStdDomainReference.java | 45 +++ .../reference/XLangXPrototypeReference.java | 6 +- .../reference/XLangXdefKeyAttrReference.java | 4 +- .../reference/XLangXdefNameReference.java | 44 +++ .../idea/plugin/resource/EnumDictBean.java | 9 + .../plugin/resource/ProjectDictProvider.java | 2 +- .../nop/idea/plugin/utils/PsiClassHelper.java | 9 + .../src/main/resources/META-INF/plugin.xml | 2 +- .../messages/NopPluginBundle.properties | 44 +-- .../messages/NopPluginBundle_zh.properties | 44 +-- .../idea/plugin/lang/TestXLangReferences.java | 12 + .../test/resources/_vfs/test/doc/example.xdef | 1 + .../test/resources/_vfs/test/doc/example.xdoc | 4 +- 20 files changed, 449 insertions(+), 250 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/{annotation => lang}/XLangAnnotator.java (45%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefNameReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictBean.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangAnnotator.java similarity index 45% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangAnnotator.java index a0f0ebdee..280647e09 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangAnnotator.java @@ -5,32 +5,29 @@ * Gitee: https://gitee.com/canonical-entropy/nop-entropy * Github: https://github.com/entropy-cloud/nop-entropy */ -package io.nop.idea.plugin.annotation; +package io.nop.idea.plugin.lang; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.Annotator; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; -import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlText; import com.intellij.psi.xml.XmlToken; import com.intellij.xml.util.XmlTagUtil; -import io.nop.api.core.beans.DictBean; import io.nop.api.core.config.AppConfig; import io.nop.api.core.validate.IValidationErrorCollector; import io.nop.commons.util.StringHelper; -import io.nop.core.dict.DictProvider; import io.nop.core.exceptions.ErrorMessageManager; +import io.nop.idea.plugin.lang.psi.XLangAttribute; +import io.nop.idea.plugin.lang.psi.XLangAttributeValue; import io.nop.idea.plugin.lang.reference.XLangReference; import io.nop.idea.plugin.messages.NopPluginBundle; -import io.nop.idea.plugin.reference.XLangVfsFileReference; -import io.nop.idea.plugin.reference.XLangElementReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -39,7 +36,6 @@ import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xdef.IStdDomainHandler; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefNode; -import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.domain.StdDomainRegistry; import org.jetbrains.annotations.NotNull; @@ -48,6 +44,10 @@ public class XLangAnnotator implements Annotator { @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (!(element instanceof XmlElement)) { + return; + } + ProjectEnv.withProject(element.getProject(), () -> { try { doAnnotate(element, holder); @@ -76,31 +76,29 @@ public class XLangAnnotator implements Annotator { XmlTag tag = (XmlTag) element; XmlTagInfo tagInfo = getTagInfo(tag); - if (tagInfo == null) + if (tagInfo == null) { return; + } // 父节点就不合法,则本节点无需处理 - if (tagInfo.getParentDefNode() == null) + if (tagInfo.getParentDefNode() == null) { return; + } checkTag(tagInfo, holder); - } else if (element instanceof XmlAttribute) { - XmlTagInfo tagInfo = getTagInfo(element); - if (tagInfo == null) - return; - XmlAttribute attr = (XmlAttribute) element; - checkAttr(holder, attr, tagInfo); - } else if (element instanceof XmlAttributeValue) { - XmlTagInfo tagInfo = getTagInfo(element); - if (tagInfo == null || tagInfo.getDefNode() == null) + } // + else if (element instanceof XLangAttribute attr) { + checkAttr(holder, attr); + } // + else if (element instanceof XLangAttributeValue attrValue) { + XLangAttribute attr = attrValue.getParentAttr(); + IXDefAttribute attrDef = attr != null ? attr.getDefAttr() : null; + if (attrDef == null) { return; - String attrName = XmlPsiHelper.getAttrName((XmlAttributeValue) element); - if (!StringHelper.isBlank(attrName)) { - XDefTypeDecl attrType = getAttrType(attrName, tagInfo); - if (attrType != null) { - checkAttrValue(holder, attrType, (XmlAttributeValue) element); - } } + + XDefTypeDecl attrType = attrDef.getType(); + checkAttrValue(holder, attr, attrType, attrValue); } } @@ -114,10 +112,11 @@ public class XLangAnnotator implements Annotator { continue; } + TextRange textRange = ref.getAbsoluteRange(); PsiElement target = ref.resolve(); if (target instanceof NopVirtualFile vfs && vfs.forFileChildren()) { holder.newSilentAnnotation(HighlightSeverity.INFORMATION) - .range(reference.getAbsoluteRange()) + .range(textRange) .textAttributes(DefaultLanguageHighlighterColors.DOC_COMMENT_TAG_VALUE) .create(); } @@ -125,150 +124,108 @@ public class XLangAnnotator implements Annotator { String msg = ref.getUnresolvedMessage(); if (target == null && msg != null) { holder.newAnnotation(HighlightSeverity.ERROR, msg) - .range(ref.getAbsoluteRange()) + .range(textRange) .highlightType(ProblemHighlightType.ERROR) .create(); } } } - private boolean checkTag(XmlTagInfo tagInfo, AnnotationHolder holder) { + private void checkTag(XmlTagInfo tagInfo, AnnotationHolder holder) { XmlTag tag = tagInfo.getTag(); String tagNameStr = tag.getName(); IXDefNode defNode = tagInfo.getDefNode(); if (defNode == null) { if (tagInfo.isCustom() || tagInfo.isAllowedUnknownName(tagNameStr)) { - return false; + return; } XmlToken startTagName = XmlTagUtil.getStartTagNameElement(tag); - holder.newAnnotation(HighlightSeverity.ERROR, NopPluginBundle.message("xlang.annotation.invalid.tag", tag.getName())) - .range(startTagName) - .highlightType(ProblemHighlightType.ERROR) - .create(); + holder.newAnnotation(HighlightSeverity.ERROR, + NopPluginBundle.message("xlang.annotation.invalid.tag", tag.getName())) + .range(startTagName) + .highlightType(ProblemHighlightType.ERROR) + .create(); XmlToken endTagName = XmlTagUtil.getEndTagNameElement(tag); if (endTagName != null && endTagName.getText().equals(tagNameStr)) { - holder.newAnnotation(HighlightSeverity.ERROR, NopPluginBundle.message("xlang.annotation.invalid.tag", tag.getName())) - .range(endTagName) - .highlightType(ProblemHighlightType.ERROR) - .create(); + holder.newAnnotation(HighlightSeverity.ERROR, + NopPluginBundle.message("xlang.annotation.invalid.tag", tag.getName())) + .range(endTagName) + .highlightType(ProblemHighlightType.ERROR) + .create(); } } else { - for (XmlAttribute attr : tag.getAttributes()) { - checkAttr(holder, attr, tagInfo); - } - - return checkValue(holder, tag, defNode); + checkValue(holder, tag, defNode); } - - return true; } - private void checkAttr(AnnotationHolder holder, @NotNull XmlAttribute attr, - XmlTagInfo tagInfo) { - String attrName = attr.getName(); - if (StringHelper.isBlank(attrName)) + private void checkAttr(AnnotationHolder holder, @NotNull XLangAttribute attr) { + XmlElement attrNameElement = attr.getNameElement(); + if (attrNameElement == null // + || "xmlns".equals(attrNameElement.getText()) // + || "xmlns".equals(attr.getNamespacePrefix()) // + ) { return; + } - XDefTypeDecl attrType = getAttrType(attrName, tagInfo); - - if (attrType == null) { - if (tagInfo.isAllowedUnknownName(attrName)) - return; - - if (attrName.equals("xmlns") || attrName.startsWith("xmlns:")) - return; - - holder.newAnnotation(HighlightSeverity.ERROR, NopPluginBundle.message("xlang.annotation.invalid.attr.name", attr.getName())) - .range(attr.getNameElement()) - .highlightType(ProblemHighlightType.ERROR) - .create(); - } else { - if (attr.getValueElement() != null) - checkAttrValue(holder, attrType, attr.getValueElement()); + if (attr.getDefAttr() == null) { + holder.newAnnotation(HighlightSeverity.ERROR, + NopPluginBundle.message("xlang.annotation.attr.not-defined", attr.getName())) + .range(attrNameElement) + .highlightType(ProblemHighlightType.ERROR) + .create(); } } - private XDefTypeDecl getAttrType(String attrName, XmlTagInfo tagInfo) { - IXDefNode defNode = tagInfo.getDefNode(); + private void checkAttrValue( + AnnotationHolder holder, XLangAttribute attr, XDefTypeDecl attrType, XLangAttributeValue attrValue + ) { + String attrName = attr.getName(); + String attrValueText = attrValue.getValue(); + TextRange attrValueTextRange = attrValue.getValueTextRange(); - XDefTypeDecl attrType = null; - if (attrName.startsWith("xdsl:")) { - attrName = "x:" + attrName.substring("xdsl:".length()); - } - IXDefAttribute defAttr = null; - // 识别系统保留名字空间 - if (attrName.startsWith("x:")) { - if (tagInfo.getXDslDefNode() != null) { - defAttr = tagInfo.getXDslDefNode().getAttribute(attrName); - if (defAttr != null) - attrType = defAttr.getType(); - } - } else if (attrName.startsWith("xpl:")) { - if (defNode != null) { - defAttr = defNode.getAttribute(attrName); - if (defAttr != null) - attrType = defAttr.getType(); - } - } else { - if (defNode != null) { - attrType = defNode.getAttrType(attrName); + if (StringHelper.isEmpty(attrValueText)) { + if (attrType.isMandatory()) { + holder.newAnnotation(HighlightSeverity.ERROR, + NopPluginBundle.message("xlang.annotation.attr.value-required", attrName)) + .range(attrValue.getTextRange()) + .highlightType(ProblemHighlightType.ERROR) + .create(); } + return; } - return attrType; - } - private void checkAttrValue(AnnotationHolder holder, XDefTypeDecl attrType, XmlAttributeValue element) { - XmlAttribute attr = (XmlAttribute) element.getParent(); - String value = attr.getValue(); - if (StringHelper.isEmpty(value)) { - if (attrType.isMandatory()) { - holder.newAnnotation(HighlightSeverity.ERROR, NopPluginBundle.message("xlang.annotation.attr.not-allow-empty", attr.getName())) - .range(element) - .highlightType(ProblemHighlightType.ERROR) - .create(); - } + // Note: dict/enum 的有效值检查由 PsiReference 处理 + attrValueText = StringHelper.unescapeXml(attrValueText); + + IStdDomainHandler domainHandler = StdDomainRegistry.instance().getStdDomainHandler(attrType.getStdDomain()); + if (domainHandler == null) { return; } - value = StringHelper.unescapeXml(value); - - if (attrType.getStdDomain().equals(XDefConstants.STD_DOMAIN_ENUM)) { - String dictName = attrType.getOptions(); - if (dictName != null) { - DictBean dict = DictProvider.instance().getDict(null, dictName, null,null); - if (dict != null) { - if (dict.getOptionByValue(value) == null) { - String desc = "value not in " + dict.getValues(); - holder.newAnnotation(HighlightSeverity.ERROR, desc) - .range(element) - .highlightType(ProblemHighlightType.ERROR) - .create(); - } - } - } - } else { - IStdDomainHandler domainHandler = StdDomainRegistry.instance().getStdDomainHandler(attrType.getStdDomain()); - if (domainHandler != null) { - try { - domainHandler.validate(XmlPsiHelper.getLocation(element), attr.getName(), value, IValidationErrorCollector.THROW_ERROR); - } catch (Exception e) { - String desc = ErrorMessageManager.instance().buildErrorMessage(AppConfig.defaultLocale(), e, - false, false, false).getDescription(); - if (desc == null) - desc = e.getMessage(); - holder.newAnnotation(HighlightSeverity.ERROR, "err:" + desc) - .range(element) - .highlightType(ProblemHighlightType.ERROR) - .create(); - } + try { + domainHandler.validate(XmlPsiHelper.getLocation(attrValue), + attrName, + attrValueText, + IValidationErrorCollector.THROW_ERROR); + } catch (Exception e) { + String msg = ErrorMessageManager.instance() + .buildErrorMessage(AppConfig.defaultLocale(), e, false, false, false) + .getDescription(); + if (msg == null) { + msg = e.getMessage(); } + + holder.newAnnotation(HighlightSeverity.ERROR, msg) + .range(attrValueTextRange) + .highlightType(ProblemHighlightType.ERROR) + .create(); } } - private boolean checkValue(AnnotationHolder holder, XmlTag tag, IXDefNode defNode) { + private void checkValue(AnnotationHolder holder, XmlTag tag, IXDefNode defNode) { String tagValue = getTagValue(tag); @@ -276,19 +233,22 @@ public class XLangAnnotator implements Annotator { if (!defNode.hasChild() && !defNode.getXdefValue().isSupportBody(StdDomainRegistry.instance())) { // 不允许子节点 if (XmlPsiHelper.hasChild(tag)) { - holder.newAnnotation(HighlightSeverity.ERROR, NopPluginBundle.message("xlang.annotation.tag.not-allow-child", tag.getName())) - .range(tag.getValue().getTextRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); - return false; + holder.newAnnotation(HighlightSeverity.ERROR, + NopPluginBundle.message("xlang.annotation.tag.not-allow-child", tag.getName())) + .range(tag.getValue().getTextRange()) + .highlightType(ProblemHighlightType.ERROR) + .create(); + return; } } if (defNode.getXdefValue().isMandatory()) { if (StringHelper.isBlank(tagValue)) { - holder.newAnnotation(HighlightSeverity.ERROR, NopPluginBundle.message("xlang.annotation.value.not-allow-empty", tag.getName())) - .range(getStartTagName(tag)) - .highlightType(ProblemHighlightType.ERROR) - .create(); + holder.newAnnotation(HighlightSeverity.ERROR, + NopPluginBundle.message("xlang.annotation.value.not-allow-empty", + tag.getName())) + .range(getStartTagName(tag)) + .highlightType(ProblemHighlightType.ERROR) + .create(); } } if (!StringHelper.isBlank(tagValue)) { @@ -296,27 +256,34 @@ public class XLangAnnotator implements Annotator { IStdDomainHandler domainHandler = StdDomainRegistry.instance().getStdDomainHandler(domain); if (domainHandler != null) { try { - domainHandler.validate(XmlPsiHelper.getValueLocation(tag), "body",tagValue, IValidationErrorCollector.THROW_ERROR); + domainHandler.validate(XmlPsiHelper.getValueLocation(tag), + "body", + tagValue, + IValidationErrorCollector.THROW_ERROR); } catch (Exception e) { holder.newAnnotation(HighlightSeverity.ERROR, - "err:" + ErrorMessageManager.instance().buildErrorMessage(AppConfig.defaultLocale(), e, - false, false, false).getDescription()) - .range(tag.getValue().getTextRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); + "err:" + ErrorMessageManager.instance() + .buildErrorMessage(AppConfig.defaultLocale(), + e, + false, + false, + false) + .getDescription()) + .range(tag.getValue().getTextRange()) + .highlightType(ProblemHighlightType.ERROR) + .create(); } } } } else { if (!StringHelper.isBlank(tagValue)) { - holder.newAnnotation(HighlightSeverity.ERROR, NopPluginBundle.message("xlang.annotation.tag.not-allow-value", tag.getName())) - .range(tag.getTextRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); + holder.newAnnotation(HighlightSeverity.ERROR, + NopPluginBundle.message("xlang.annotation.tag.not-allow-value", tag.getName())) + .range(tag.getTextRange()) + .highlightType(ProblemHighlightType.ERROR) + .create(); } } - - return true; } String getTagValue(XmlTag tag) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java index f27541eb0..078e5c287 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -22,6 +22,10 @@ public class XLangAttribute extends XmlAttributeImpl { return getClass().getSimpleName() + ':' + getElementType() + "('" + getName() + "')"; } + public XLangTag getParentTag() { + return (XLangTag) getParent(); + } + @Override public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { // 参考 XmlAttributeDelegate#getDefaultReferences @@ -46,7 +50,7 @@ public class XLangAttribute extends XmlAttributeImpl { /** 获取当前属性在元模型中的定义 */ public IXDefAttribute getDefAttr() { - XLangTag tag = (XLangTag) getParent(); + XLangTag tag = getParentTag(); if (tag == null) { return null; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index 4547ed6d2..34481a6fd 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -9,9 +9,9 @@ import io.nop.idea.plugin.lang.reference.XLangParentTagAttrReference; import io.nop.idea.plugin.lang.reference.XLangReferenceHelper; import io.nop.idea.plugin.lang.reference.XLangXPrototypeReference; import io.nop.idea.plugin.lang.reference.XLangXdefKeyAttrReference; +import io.nop.idea.plugin.lang.reference.XLangXdefNameReference; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.XDefKeys; -import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdsl.XDslKeys; import org.jetbrains.annotations.NotNull; @@ -28,6 +28,10 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { return getClass().getSimpleName(); } + public XLangAttribute getParentAttr() { + return getParent() instanceof XLangAttribute p ? p : null; + } + @Override public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { String attrValue = getValue(); @@ -35,22 +39,41 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { return PsiReference.EMPTY_ARRAY; } - if (!(getParent() instanceof XLangAttribute attr)) { + XLangAttribute attr = getParentAttr(); + if (attr == null) { return PsiReference.EMPTY_ARRAY; } - String attrName = attr.getName(); IXDefAttribute attrDef = attr.getDefAttr(); // 对于未定义属性,不做引用识别 if (attrDef == null) { return PsiReference.EMPTY_ARRAY; } - XLangTag tag = (XLangTag) attr.getParent(); + // 根据属性名,从属性值中查找引用 + PsiReference[] refs = getReferencesByAttrName(attr, attrValue); + if (refs != null) { + return refs; + } + + // 根据属性定义类型,从属性值中查找引用 + refs = XLangReferenceHelper.getReferencesByAttrDefType(this, attrValue, attrDef.getType()); + if (refs != null) { + return refs; + } + + // TODO 其他引用识别 + // + // + return XLangReferenceHelper.getReferencesFromText(this, attrValue); + } + + private PsiReference[] getReferencesByAttrName(XLangAttribute attr, String attrValue) { + XLangTag tag = attr.getParentTag(); XDslKeys xdslKeys = tag.getXDslKeys(); XDefKeys xdefKeys = tag.getXDefKeys(); - // 根据属性名,从属性值中查找引用 + String attrName = attr.getName(); // Note: XmlAttributeValue 的文本范围是包含引号的 TextRange attrValueTextRange = getValueTextRange().shiftLeft(getStartOffset()); if (xdslKeys.PROTOTYPE.equals(attrName)) { @@ -69,18 +92,21 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { return new PsiReference[] { new XLangParentTagAttrReference(this, attrValueTextRange, attrValue) }; - } + } // + else if (xdefKeys.NAME.equals(attrName)) { + // 与根节点上的 xdef:bean-package 组成 class + XLangTag rootTag = tag.getRootTag(); - // 根据属性定义类型,从属性值中查找引用 - XDefTypeDecl attrDefType = attrDef.getType(); - PsiReference[] refs = XLangReferenceHelper.getReferencesByAttrDefType(this, attrValue, attrDefType); - if (refs != null) { - return refs; + String pkgName = rootTag.getAttributeValue(rootTag.getXDefKeys().BEAN_PACKAGE); + if (StringHelper.isEmpty(pkgName)) { + return PsiReference.EMPTY_ARRAY; + } + + return new PsiReference[] { + new XLangXdefNameReference(this, attrValueTextRange, pkgName, attrValue) + }; } - // TODO 其他引用识别 - // - // - return XLangReferenceHelper.getReferencesFromText(this, attrValue); + return null; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index fc3359dcb..6bbbcd2e1 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -44,6 +44,23 @@ public class XLangTag extends XmlTagImpl { return getClass().getSimpleName() + ':' + getElementType() + "('" + getName() + "')"; } + @Override + public XLangTag getParentTag() { + return (XLangTag) super.getParentTag(); + } + + public XLangTag getRootTag() { + XLangTag tag = this; + + do { + XLangTag parent = tag.getParentTag(); + if (parent == null) { + return tag; + } + tag = parent; + } while (true); + } + @Override public void clearCaches() { this.schemaMeta = null; @@ -133,6 +150,7 @@ public class XLangTag extends XmlTagImpl { return getSchemaMeta().selfDefNode; } + /** 获取 {@link #getSelfDefNode()} 上指定的属性 */ public IXDefAttribute getSelfDefNodeAttr(String attrName) { return getXDefNodeAttr(getSelfDefNode(), attrName); } @@ -171,7 +189,14 @@ public class XLangTag extends XmlTagImpl { /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ private IXDefAttribute getXDefNodeAttr(IXDefNode xdefNode, String attrName) { - if (attrName.startsWith("xmlns:")) { + String xmlnsPrefix = "xmlns:"; + if (attrName.startsWith(xmlnsPrefix)) { + String attrValue = getAttributeValue(attrName); + // 忽略 xmlns:biz="biz" 形式的属性 + if (attrName.equals(xmlnsPrefix + attrValue)) { + return null; + } + XDefAttribute at = new XDefAttribute(); at.setName(attrName); at.setType(STD_DOMAIN_XDEF_REF); @@ -243,13 +268,16 @@ public class XLangTag extends XmlTagImpl { } catch (ProcessCanceledException e) { // Note: 若处理被中断,则保持元模型信息为空,以便于后续再重新初始化 schemaMeta = null; + + // Note: 避免后续访问成员变量出现 NPE 问题 + return SchemaMeta.UNKNOWN; } } return schemaMeta; } private SchemaMeta createSchemaMeta() { - XLangTag parentTag = (XLangTag) getParentTag(); + XLangTag parentTag = getParentTag(); if (parentTag == null) { return createSchemaMetaForRootTag(this); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java index d0bd4a3ab..d2e9c10eb 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java @@ -1,23 +1,15 @@ package io.nop.idea.plugin.lang.reference; -import java.util.function.Function; - -import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.impl.source.tree.LeafPsiElement; -import com.intellij.psi.util.PsiTreeUtil; import io.nop.api.core.beans.DictBean; -import io.nop.api.core.beans.DictOptionBean; import io.nop.core.dict.DictProvider; import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.resource.EnumDictBean; import io.nop.idea.plugin.resource.EnumDictOptionBean; import io.nop.idea.plugin.resource.ProjectEnv; -import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFile; import org.jetbrains.annotations.Nullable; -import org.jetbrains.yaml.psi.YAMLKeyValue; /** * 对字典选项的引用 @@ -49,7 +41,7 @@ public class XLangDictOptionReference extends XLangReferenceBase { public DictBean getDictBean() { if (dictBean == null) { - dictBean = ProjectEnv.withProject(getElement().getProject(), + dictBean = ProjectEnv.withProject(myElement.getProject(), () -> DictProvider.instance().getDict(null, dictName, null, null)); } return dictBean; @@ -62,30 +54,25 @@ public class XLangDictOptionReference extends XLangReferenceBase { return null; } - DictOptionBean dictOpt = dictBean.getOptionByValue(dictOptionValue); - if (dictOpt instanceof EnumDictOptionBean opt) { - return opt.target; - } else { - Function targetResolver = // - (file) -> XmlPsiHelper.findFirstElement(file, (element) -> { - if (element instanceof LeafPsiElement value // - && dictOptionValue.equals(value.getText()) // - ) { - PsiElement parent = // - PsiTreeUtil.getParentOfType(element, YAMLKeyValue.class); - PsiElement key = parent != null ? parent.getFirstChild() : null; + if (dictBean instanceof EnumDictBean) { + EnumDictOptionBean dictOpt = (EnumDictOptionBean) dictBean.getOptionByValue(dictOptionValue); - return key != null && "value".equals(key.getText()); - } - return false; - }); + if (dictOpt != null) { + return dictOpt.target; + } - Project project = getElement().getProject(); - String path = "/dict/" + dictName + ".dict.yaml"; + String msg = NopPluginBundle.message("xlang.annotation.reference.enum-option-not-defined", + dictOptionValue, + dictBean.getValues()); + setUnresolvedMessage(msg); - NopVirtualFile target = new NopVirtualFile(project, path, dictOptionValue != null ? targetResolver : null); + return null; + } // + else { + NopVirtualFile target = XLangReferenceHelper.createNopVfsForDict(myElement, dictName, dictOptionValue); if (target.hasEmptyChildren()) { + String path = target.getPath(); String msg = dictOptionValue != null // ? NopPluginBundle.message("xlang.annotation.reference.dict-option-not-defined", @@ -93,6 +80,8 @@ public class XLangDictOptionReference extends XLangReferenceBase { path) : NopPluginBundle.message("xlang.annotation.reference.dict-yaml-not-found", path); setUnresolvedMessage(msg); + + return null; } return target; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java index ab5bc0abc..ffa92812e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java @@ -26,7 +26,9 @@ public class XLangParentTagAttrReference extends XLangReferenceBase { @Override public @Nullable PsiElement resolveInner() { XLangTag tag = PsiTreeUtil.getParentOfType(myElement, XLangTag.class); - assert tag != null; + if (tag == null) { + return null; + } XmlAttribute target = tag.getAttribute(attrValue); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java index 571bd9dbe..d93eb93f0 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -1,23 +1,34 @@ package io.nop.idea.plugin.lang.reference; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Function; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlElement; import io.nop.commons.text.MutableString; import io.nop.commons.text.tokenizer.TextScanner; import io.nop.commons.util.StringHelper; import io.nop.core.type.IGenericType; import io.nop.idea.plugin.utils.PsiClassHelper; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.idea.plugin.vfs.NopVirtualFileReference; import io.nop.xlang.xdef.XDefTypeDecl; +import io.nop.xlang.xdef.domain.StdDomainRegistry; import io.nop.xlang.xdsl.XDslParseHelper; +import org.jetbrains.yaml.psi.YAMLKeyValue; import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_CLASS_NAME; import static io.nop.xlang.xdef.XDefConstants.STD_DOMAIN_CLASS_NAME_SET; @@ -66,8 +77,10 @@ public class XLangReferenceHelper { getReferencesFromVfsPathCsv(refElement, refValue, textRangeOffset); case STD_DOMAIN_GENERIC_TYPE, STD_DOMAIN_GENERIC_TYPE_LIST -> // getReferencesFromGenericTypeCsv(refElement, refValue, textRangeOffset); - case STD_DOMAIN_CLASS_NAME, STD_DOMAIN_CLASS_NAME_SET, STD_DOMAIN_PACKAGE_NAME -> // + case STD_DOMAIN_CLASS_NAME, STD_DOMAIN_CLASS_NAME_SET -> // PsiClassHelper.createJavaClassReferences(refElement, refValue, textRangeOffset); + case STD_DOMAIN_PACKAGE_NAME -> // + PsiClassHelper.createPackageReferences(refElement, refValue, textRangeOffset); case STD_DOMAIN_DICT, STD_DOMAIN_ENUM -> // new PsiReference[] { new XLangDictOptionReference(refElement, textRange, attrDefType.getOptions(), refValue) @@ -117,7 +130,7 @@ public class XLangReferenceHelper { // 引用数据域的类型定义 TextRange textRange = new TextRange(0, stdDomain.length()).shiftRight(textRangeOffset + stdDomainIndex); - refs.add(new XLangDictOptionReference(refElement, textRange, "core/std-domain", stdDomain)); + refs.add(new XLangStdDomainReference(refElement, textRange, stdDomain)); if (optionsIndex > 0) { int offset = textRangeOffset + optionsIndex; @@ -217,6 +230,46 @@ public class XLangReferenceHelper { return list.toArray(PsiReference[]::new); } + public static NopVirtualFile createNopVfsForDict( + PsiElement refElement, String dictName, Object dictOptionValue + ) { + Project project = refElement.getProject(); + + Function targetResolver = // + (file) -> XmlPsiHelper.findFirstElement(file, (element) -> { + if (element instanceof LeafPsiElement value // + && dictOptionValue.equals(value.getText()) // + ) { + PsiElement parent = // + PsiTreeUtil.getParentOfType(element, YAMLKeyValue.class); + PsiElement key = parent != null ? parent.getFirstChild() : null; + + return key != null && "value".equals(key.getText()); + } + return false; + }); + + String path = "/dict/" + dictName + ".dict.yaml"; + + return new NopVirtualFile(project, path, dictOptionValue != null ? targetResolver : null); + } + + public static List getRegisteredStdDomains() { + StdDomainRegistry registry = StdDomainRegistry.instance(); + + try { + Field field = registry.getClass().getDeclaredField("domainHandlers"); + field.setAccessible(true); + + List result = new ArrayList<>(((Map) field.get(registry)).keySet()); + Collections.sort(result); + + return result; + } catch (Exception ignore) { + return new ArrayList<>(); + } + } + private static Map extractValuesFromCsv(String csv) { Map rangePathMap = new HashMap<>(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainReference.java new file mode 100644 index 000000000..4f8a11e3d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainReference.java @@ -0,0 +1,45 @@ +package io.nop.idea.plugin.lang.reference; + +import java.util.List; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.xlang.xdef.IStdDomainHandler; +import io.nop.xlang.xdef.domain.StdDomainRegistry; +import org.jetbrains.annotations.Nullable; + +/** + * 对标准数据域的引用 + * + * @author flytreeleft + * @date 2025-07-15 + */ +public class XLangStdDomainReference extends XLangReferenceBase { + private final String stdDomain; + + public XLangStdDomainReference( + PsiElement myElement, TextRange myRangeInElement, // + String stdDomain + ) { + super(myElement, myRangeInElement); + this.stdDomain = stdDomain; + } + + @Override + public @Nullable PsiElement resolveInner() { + IStdDomainHandler domainHandler = StdDomainRegistry.instance().getStdDomainHandler(stdDomain); + + if (domainHandler == null) { + List options = XLangReferenceHelper.getRegisteredStdDomains(); + String msg = NopPluginBundle.message("xlang.annotation.reference.std-domain-not-registered", + stdDomain, + options); + setUnresolvedMessage(msg); + + return null; + } + + return XLangReferenceHelper.createNopVfsForDict(myElement, "core/std-domain", stdDomain); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java index 1ab2c2ce9..9604cb075 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java @@ -27,9 +27,11 @@ public class XLangXPrototypeReference extends XLangReferenceBase { @Override public @Nullable PsiElement resolveInner() { XLangTag tag = PsiTreeUtil.getParentOfType(myElement, XLangTag.class); - assert tag != null; + if (tag == null) { + return null; + } - XLangTag parentTag = (XLangTag) tag.getParentTag(); + XLangTag parentTag = tag.getParentTag(); if (parentTag == null) { String msg = NopPluginBundle.message("xlang.annotation.reference.x-prototype-no-parent"); setUnresolvedMessage(msg); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java index ec23fdd98..c0977466a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java @@ -42,7 +42,9 @@ public class XLangXdefKeyAttrReference extends XLangReferenceBase implements Psi @Override public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { XLangTag tag = PsiTreeUtil.getParentOfType(myElement, XLangTag.class); - assert tag != null; + if (tag == null) { + return ResolveResult.EMPTY_ARRAY; + } return XmlPsiHelper.getAttrsFromChildTag(tag, attrValue).stream() // .map(PsiElementResolveResult::new) // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefNameReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefNameReference.java new file mode 100644 index 000000000..cd9d19ec6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefNameReference.java @@ -0,0 +1,44 @@ +package io.nop.idea.plugin.lang.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.PsiClassHelper; +import org.jetbrains.annotations.Nullable; + +/** + * {@link io.nop.xlang.xdef.XDefKeys#NAME xdef:name} 的值引用 + * + * @author flytreeleft + * @date 2025-07-15 + */ +public class XLangXdefNameReference extends XLangReferenceBase { + private final String beanPackage; + private final String attrValue; + + public XLangXdefNameReference( + XmlElement myElement, TextRange myRangeInElement, // + String beanPackage, String attrValue + ) { + super(myElement, myRangeInElement); + this.beanPackage = beanPackage; + this.attrValue = attrValue; + } + + @Override + public @Nullable PsiElement resolveInner() { + String className = beanPackage + '.' + attrValue; + + PsiClass clazz = PsiClassHelper.findClass(myElement, className); + + if (clazz == null) { + String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-name-class-not-found", className); + setUnresolvedMessage(msg); + + return null; + } + return clazz; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictBean.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictBean.java new file mode 100644 index 000000000..0ffde4bd3 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictBean.java @@ -0,0 +1,9 @@ +package io.nop.idea.plugin.resource; + +import io.nop.api.core.beans.DictBean; + +/** + * @author flytreeleft + * @date 2025-07-15 + */ +public class EnumDictBean extends DictBean {} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java index e38eca98e..dd1035c95 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java @@ -87,7 +87,7 @@ public class ProjectDictProvider implements IDictProvider { options.add(option); } - DictBean dict = new DictBean(); + DictBean dict = new EnumDictBean(); dict.setOptions(options); DictModel ret = new DictModel(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java index ee103ab68..d05812c5b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -36,6 +36,7 @@ import com.intellij.psi.PsiTypes; import com.intellij.psi.PsiWildcardType; import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceProvider; import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceSet; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.PackageReferenceSet; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.searches.ClassInheritorsSearch; import com.intellij.psi.util.PsiUtil; @@ -102,6 +103,14 @@ public class PsiClassHelper { return refSet.getReferences(); } + public static PsiReference @NotNull [] createPackageReferences( + PsiElement element, String qualifiedName, int startInElement + ) { + PackageReferenceSet refSet = new PackageReferenceSet(qualifiedName, element, startInElement); + + return refSet.getPsiReferences(); + } + /** 得到 {@link PsiType} 对应的 {@link PsiClass} */ public static PsiClass getTypeClass(PsiElement context, PsiType type) { if (type == null) { diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 1d6e530ab..1b7fe5dd4 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -87,7 +87,7 @@ implementationClass="io.nop.idea.plugin.lang.XLangParserDefinition"/> + implementationClass="io.nop.idea.plugin.lang.XLangAnnotator"/> diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties index b0052f29e..d1c107813 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties @@ -1,21 +1,23 @@ -line.breakpoints.tab.title=XLang line breakpoints -xlang.annotation.invalid.tag=Invalid tag name: ''{0}'' -xlang.annotation.invalid.attr.name=Invalid attr name: ''{0}'' -xlang.annotation.invalid.attr.value=Invalid attr value: ''{0}'' -xlang.annotation.attr.not-allow-empty=Attr ''{0}'' not allow empty -xlang.annotation.value.not-allow-empty=Tag ''{0}'' value not allow empty -xlang.annotation.tag.not-allow-value=Tag ''{0}'' not allow value -xlang.annotation.tag.not-allow-child=Tag ''{0}'' not allow child -xlang.annotation.reference.vfs-file-not-found=The referenced vfs file ''{0}'' doesn''t exist -xlang.annotation.reference.dict-yaml-not-found=The dict yaml file ''{0}'' doesn''t exist -xlang.annotation.reference.dict-option-not-defined=The dict option ''{0}'' isn''t defined in ''{1}'' -xlang.annotation.reference.xdef-ref-not-found=No xdef defined node named ''{0}'' exists -xlang.annotation.reference.xdef-ref-not-found-in-path=No xdef defined node named ''{0}'' in ''{1}'' -xlang.annotation.reference.parent-tag-attr-not-found=No attribute named ''{0}'' in current node -xlang.annotation.reference.xdef-key-attr-not-found=No child node which has attribute named ''{0}'' exists -xlang.annotation.reference.x-prototype-no-parent=Only child node can define ''x:prototype'' -xlang.annotation.reference.x-prototype-tag-not-found=No sibling node named ''{0}'' exists -xlang.annotation.reference.x-prototype-attr-not-found=No sibling node which has attribute ''{0}={1}'' exists -xlang.annotation.reference.attr-xdef-not-defined=Undefined attribute ''{0}'' -xlang.doc.markdown.link-title='{'Link'}'{0} -xlang.doc.markdown.image-title='{'Image'}'{0} +line.breakpoints.tab.title = XLang line breakpoints +xlang.annotation.invalid.tag = Invalid tag name: ''{0}'' +xlang.annotation.attr.not-defined = The attribute named ''{0}'' isn''t defined +xlang.annotation.attr.value-required = The attribute named ''{0}'' can not have empty value +xlang.annotation.value.not-allow-empty = Tag ''{0}'' value not allow empty +xlang.annotation.tag.not-allow-value = Tag ''{0}'' not allow value +xlang.annotation.tag.not-allow-child = Tag ''{0}'' not allow child +xlang.annotation.reference.vfs-file-not-found = The referenced vfs file ''{0}'' doesn''t exist +xlang.annotation.reference.dict-yaml-not-found = The dict yaml file ''{0}'' doesn''t exist +xlang.annotation.reference.dict-option-not-defined = The dict option ''{0}'' isn''t defined in ''{1}'' +xlang.annotation.reference.enum-option-not-defined = The enum item ''{0}'' isn''t listed in {1} +xlang.annotation.reference.xdef-ref-not-found = No xdef defined node named ''{0}'' exists +xlang.annotation.reference.xdef-ref-not-found-in-path = No xdef defined node named ''{0}'' in ''{1}'' +xlang.annotation.reference.parent-tag-attr-not-found = No attribute named ''{0}'' in current node +xlang.annotation.reference.xdef-key-attr-not-found = No child node which has attribute named ''{0}'' exists +xlang.annotation.reference.x-prototype-no-parent = Only child node can define ''x:prototype'' +xlang.annotation.reference.x-prototype-tag-not-found = No sibling node named ''{0}'' exists +xlang.annotation.reference.x-prototype-attr-not-found = No sibling node which has attribute ''{0}={1}'' exists +xlang.annotation.reference.attr-xdef-not-defined = Undefined attribute ''{0}'' +xlang.annotation.reference.std-domain-not-registered = The std-domain ''{0}'' isn''t listed in {1} +xlang.annotation.reference.xdef-name-class-not-found = The Java class ''{0}'' isn''t created or generated +xlang.doc.markdown.link-title = '{'Link'}'{0} +xlang.doc.markdown.image-title = '{'Image'}'{0} diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties index 4c23a1985..6df121bda 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties @@ -1,21 +1,23 @@ -line.breakpoints.tab.title=XLang line breakpoints -xlang.annotation.invalid.tag=\u975E\u6CD5\u7684\u6807\u7B7E\u540D: ''{0}'' -xlang.annotation.invalid.attr.name=\u975E\u6CD5\u7684\u5C5E\u6027\u540D: ''{0}'' -xlang.annotation.invalid.attr.value=\u975E\u6CD5\u7684\u5C5E\u6027\u503C: ''{0}'' -xlang.annotation.attr.not-allow-empty=\u5C5E\u6027 ''{0}'' \u4E0D\u5141\u8BB8\u4E3A\u7A7A -xlang.annotation.value.not-allow-empty=\u6807\u7B7E ''{0}'' \u7684\u503C\u4E0D\u5141\u8BB8\u4E3A\u7A7A -xlang.annotation.tag.not-allow-value=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5185\u5BB9\u8282\u70B9 -xlang.annotation.tag.not-allow-child=\u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5B50\u8282\u70B9 -xlang.annotation.reference.vfs-file-not-found=\u5F15\u7528\u7684 vfs \u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 -xlang.annotation.reference.dict-yaml-not-found=\u5B57\u5178\u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 -xlang.annotation.reference.dict-option-not-defined=\u9009\u9879 ''{0}'' \u672A\u5728\u5B57\u5178\u6587\u4EF6 ''{1}'' \u4E2D\u5B9A\u4E49 -xlang.annotation.reference.xdef-ref-not-found=\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 -xlang.annotation.reference.xdef-ref-not-found-in-path=\u5728 ''{1}'' \u4E2D\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 -xlang.annotation.reference.parent-tag-attr-not-found=\u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 -xlang.annotation.reference.xdef-key-attr-not-found=\u5728\u5B50\u8282\u70B9\u4E2D\u672A\u5B9A\u4E49\u5C5E\u6027 ''{0}'' -xlang.annotation.reference.x-prototype-no-parent=\u53EA\u6709\u5728\u5B50\u8282\u70B9\u4E0A\u624D\u80FD\u5B9A\u4E49\u5C5E\u6027 ''x:prototype'' -xlang.annotation.reference.x-prototype-tag-not-found=\u4E0D\u5B58\u5728\u6807\u7B7E\u540D\u4E3A ''{0}'' \u7684\u5144\u5F1F\u8282\u70B9 -xlang.annotation.reference.x-prototype-attr-not-found=\u4E0D\u5B58\u5728\u5C5E\u6027\u4E3A ''{0}={1}'' \u7684\u5144\u5F1F\u8282\u70B9 -xlang.annotation.reference.attr-xdef-not-defined=\u5C5E\u6027 ''{0}'' \u672A\u5B9A\u4E49 -xlang.doc.markdown.link-title='{'\u94FE\u63A5'}'{0} -xlang.doc.markdown.image-title='{'\u56FE\u7247'}'{0} +line.breakpoints.tab.title = XLang line breakpoints +xlang.annotation.invalid.tag = \u975E\u6CD5\u7684\u6807\u7B7E\u540D: ''{0}'' +xlang.annotation.attr.not-defined = \u5C5E\u6027 ''{0}'' \u672A\u5B9A\u4E49 +xlang.annotation.attr.value-required = \u5C5E\u6027 ''{0}'' \u7684\u503C\u4E0D\u5141\u8BB8\u4E3A\u7A7A +xlang.annotation.value.not-allow-empty = \u6807\u7B7E ''{0}'' \u7684\u503C\u4E0D\u5141\u8BB8\u4E3A\u7A7A +xlang.annotation.tag.not-allow-value = \u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5185\u5BB9\u8282\u70B9 +xlang.annotation.tag.not-allow-child = \u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5B50\u8282\u70B9 +xlang.annotation.reference.vfs-file-not-found = \u5F15\u7528\u7684 vfs \u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 +xlang.annotation.reference.dict-yaml-not-found = \u5B57\u5178\u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 +xlang.annotation.reference.dict-option-not-defined = \u9009\u9879 ''{0}'' \u672A\u5728\u5B57\u5178\u6587\u4EF6 ''{1}'' \u4E2D\u5B9A\u4E49 +xlang.annotation.reference.enum-option-not-defined = \u679A\u4E3E\u9879 ''{0}'' \u4E0D\u5728\u53EF\u9009\u5217\u8868 {1} \u4E2D +xlang.annotation.reference.xdef-ref-not-found = \u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 +xlang.annotation.reference.xdef-ref-not-found-in-path = \u5728 ''{1}'' \u4E2D\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684 xdef \u7247\u6BB5 +xlang.annotation.reference.parent-tag-attr-not-found = \u5728\u5F53\u524D\u8282\u70B9\u4E2D\uFF0C\u4E0D\u5B58\u5728\u540D\u5B57\u4E3A ''{0}'' \u7684\u5C5E\u6027 +xlang.annotation.reference.xdef-key-attr-not-found = \u5728\u5B50\u8282\u70B9\u4E2D\u672A\u5B9A\u4E49\u5C5E\u6027 ''{0}'' +xlang.annotation.reference.x-prototype-no-parent = \u53EA\u6709\u5728\u5B50\u8282\u70B9\u4E0A\u624D\u80FD\u5B9A\u4E49\u5C5E\u6027 ''x:prototype'' +xlang.annotation.reference.x-prototype-tag-not-found = \u4E0D\u5B58\u5728\u6807\u7B7E\u540D\u4E3A ''{0}'' \u7684\u5144\u5F1F\u8282\u70B9 +xlang.annotation.reference.x-prototype-attr-not-found = \u4E0D\u5B58\u5728\u5C5E\u6027\u4E3A ''{0}={1}'' \u7684\u5144\u5F1F\u8282\u70B9 +xlang.annotation.reference.attr-xdef-not-defined = \u5C5E\u6027 ''{0}'' \u672A\u5B9A\u4E49 +xlang.annotation.reference.std-domain-not-registered = \u6570\u636E\u57DF ''{0}'' \u4E0D\u5728\u53EF\u9009\u5217\u8868 {1} \u4E2D +xlang.annotation.reference.xdef-name-class-not-found = \u8FD8\u672A\u521B\u5EFA\u6216\u751F\u6210 Java \u7C7B ''{0}'' +xlang.doc.markdown.link-title = '{'\u94FE\u63A5'}'{0} +xlang.doc.markdown.image-title = '{'\u56FE\u7247'}'{0} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 665e436e7..0137780b6 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -418,6 +418,18 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { xdef:bean-package="io.nop.xlang.xdef" /> """, "io.nop.xlang.xdef"); + assertReference(""" + + """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + assertReference(""" + + """, null); // dict/enum 类型的值引用 assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:default-override=\"append\"", diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index 0d46380bc..f483069af 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -5,6 +5,7 @@ diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc index 2b848b428..c78d94dbb 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc @@ -1,11 +1,13 @@ - + /test/reference/test-filter.xdef, /nop/schema/xdsl.xdef + + -- Gitee From ab28a74147199ad54c155741fc69ae0c691d2a1b Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 15 Jul 2025 22:35:11 +0800 Subject: [PATCH 57/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E8=8A=82=E7=82=B9=20xdef:value=20=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E6=89=80=E6=8C=87=E5=AE=9A=E7=9A=84=E5=AD=90=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E7=B1=BB=E5=9E=8B=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86?= =?UTF-8?q?=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/lang/psi/XLangAttributeValue.java | 2 +- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 10 +++++++ .../idea/plugin/lang/psi/XLangTextToken.java | 28 +++++++++++++++++++ .../lang/reference/XLangReferenceHelper.java | 10 +++---- .../resources/_vfs/test/doc/example-1.xdoc | 9 ++++++ 5 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/doc/example-1.xdoc diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index 34481a6fd..3b1ac3c7a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -57,7 +57,7 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { } // 根据属性定义类型,从属性值中查找引用 - refs = XLangReferenceHelper.getReferencesByAttrDefType(this, attrValue, attrDef.getType()); + refs = XLangReferenceHelper.getReferencesByDefType(this, attrValue, attrDef.getType()); if (refs != null) { return refs; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 6bbbcd2e1..a04e785b8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -123,6 +123,16 @@ public class XLangTag extends XmlTagImpl { return getXDefNodeAttr(getSchemaDefNode(), attrName); } + /** + * 获取 {@link #getSchemaDefNode()} 节点的 {@link XDefKeys#VALUE xdef:value}, + * 即,其子节点(包括文本节点)对应的{@link XDefTypeDecl 类型} + */ + public XDefTypeDecl getDefNodeXdefValue() { + IXDefNode defNode = getSchemaDefNode(); + + return defNode != null ? defNode.getXdefValue() : null; + } + /** * 获取当前标签在 xdsl.xdef 中对应的节点 * diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java index 865c5de2f..179783705 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java @@ -1,7 +1,13 @@ package io.nop.idea.plugin.lang.psi; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.XmlTokenImpl; import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.lang.reference.XLangReferenceHelper; +import io.nop.xlang.xdef.XDefTypeDecl; import org.jetbrains.annotations.NotNull; /** @@ -21,4 +27,26 @@ public class XLangTextToken extends XmlTokenImpl { public String toString() { return getClass().getSimpleName() + ':' + getTokenType(); } + + public XLangTag getParentTag() { + return PsiTreeUtil.getParentOfType(this, XLangTag.class); + } + + @Override + public PsiReference @NotNull [] getReferences(PsiReferenceService.Hints hints) { + XLangTag tag = getParentTag(); + if (tag == null) { + return PsiReference.EMPTY_ARRAY; + } + + String text = getText(); + XDefTypeDecl xdefValue = tag.getDefNodeXdefValue(); + if (xdefValue == null || StringHelper.isBlank(text)) { + return PsiReference.EMPTY_ARRAY; + } + + PsiReference[] refs = XLangReferenceHelper.getReferencesByDefType(this, text, xdefValue); + + return refs != null ? refs : PsiReference.EMPTY_ARRAY; + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java index d93eb93f0..3b66b1ce8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -53,19 +53,19 @@ import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_PREFIX_OPTIONS; public class XLangReferenceHelper { /** - * 根据{@link XDefTypeDecl 属性定义类型}识别引用 + * 根据{@link XDefTypeDecl 定义类型}识别引用 * * @return 若返回 null,则表示未支持对指定类型的处理 */ - public static PsiReference[] getReferencesByAttrDefType( - XmlElement refElement, String refValue, XDefTypeDecl attrDefType + public static PsiReference[] getReferencesByDefType( + XmlElement refElement, String refValue, XDefTypeDecl refDefType ) { // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, // 从而精确匹配与引用相关的文本内容 int textRangeOffset = refElement.getText().indexOf(refValue); TextRange textRange = new TextRange(0, refValue.length()).shiftRight(textRangeOffset); - String stdDomain = attrDefType.getStdDomain(); + String stdDomain = refDefType.getStdDomain(); return switch (stdDomain) { case STD_DOMAIN_XDEF_REF -> // new PsiReference[] { @@ -83,7 +83,7 @@ public class XLangReferenceHelper { PsiClassHelper.createPackageReferences(refElement, refValue, textRangeOffset); case STD_DOMAIN_DICT, STD_DOMAIN_ENUM -> // new PsiReference[] { - new XLangDictOptionReference(refElement, textRange, attrDefType.getOptions(), refValue) + new XLangDictOptionReference(refElement, textRange, refDefType.getOptions(), refValue) }; case STD_DOMAIN_XDEF_ATTR, STD_DOMAIN_DEF_TYPE -> // getReferencesFromDefType(refElement, refValue, refValue); diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example-1.xdoc b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example-1.xdoc new file mode 100644 index 000000000..4f6685ce7 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example-1.xdoc @@ -0,0 +1,9 @@ + + + /xdsl.xdef + ]]> + -- Gitee From f9a7f38eac0c5c9da828b79612bbd53e9db8e1c9 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 16 Jul 2025 18:02:26 +0800 Subject: [PATCH 58/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E8=AF=AD=E6=B3=95=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/{ => annotator}/XLangAnnotator.java | 323 ++++++++---------- .../XLangHighlightRangeExtension.java | 20 ++ .../plugin/lang/psi/XLangAttributeValue.java | 10 +- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 68 +++- .../nop/idea/plugin/lang/psi/XLangText.java | 65 +++- .../idea/plugin/lang/psi/XLangTextToken.java | 17 +- .../idea/plugin/lang/psi/XLangValueToken.java | 8 +- .../nop/idea/plugin/utils/XmlPsiHelper.java | 27 -- .../src/main/resources/META-INF/plugin.xml | 7 +- .../messages/NopPluginBundle.properties | 22 +- .../messages/NopPluginBundle_zh.properties | 8 +- .../nop/idea/plugin/lang/TestXLangParser.java | 4 +- .../test/resources/_vfs/test/ast/xlang-1.ast | 38 ++- .../resources/_vfs/test/doc/example-1.xdoc | 19 +- .../test/resources/_vfs/test/doc/example.xdef | 4 + 15 files changed, 390 insertions(+), 250 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/{ => annotator}/XLangAnnotator.java (32%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangHighlightRangeExtension.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangAnnotator.java similarity index 32% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangAnnotator.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangAnnotator.java index 280647e09..cd9ed2231 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangAnnotator.java @@ -5,8 +5,9 @@ * Gitee: https://gitee.com/canonical-entropy/nop-entropy * Github: https://github.com/entropy-cloud/nop-entropy */ -package io.nop.idea.plugin.lang; +package io.nop.idea.plugin.lang.annotator; +import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.Annotator; @@ -17,31 +18,52 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlTag; -import com.intellij.psi.xml.XmlText; import com.intellij.psi.xml.XmlToken; import com.intellij.xml.util.XmlTagUtil; import io.nop.api.core.config.AppConfig; +import io.nop.api.core.util.SourceLocation; import io.nop.api.core.validate.IValidationErrorCollector; import io.nop.commons.util.StringHelper; import io.nop.core.exceptions.ErrorMessageManager; import io.nop.idea.plugin.lang.psi.XLangAttribute; import io.nop.idea.plugin.lang.psi.XLangAttributeValue; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.lang.psi.XLangText; +import io.nop.idea.plugin.lang.psi.XLangTextToken; import io.nop.idea.plugin.lang.reference.XLangReference; import io.nop.idea.plugin.messages.NopPluginBundle; import io.nop.idea.plugin.resource.ProjectEnv; -import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; -import io.nop.idea.plugin.utils.XmlTagInfo; import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xdef.IStdDomainHandler; import io.nop.xlang.xdef.IXDefAttribute; -import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.domain.StdDomainRegistry; import org.jetbrains.annotations.NotNull; public class XLangAnnotator implements Annotator { + /** + * 注意事项: + *

    + *
  • + * 若是 element 检查的结果包含 {@link HighlightSeverity#ERROR}, + * 则会导致其父节点不再进行检查。详细逻辑见 + * {@link com.intellij.codeInsight.daemon.impl.GeneralHighlightingPass#runVisitors GeneralHighlightingPass#runVisitors}; + *
  • + *
  • + * 检查会从 PSI 树的底部向上进行; + *
  • + *
  • + * 只能针对指定节点及其子树进行检查,而不能对父节点或兄弟节点做检查,必须确保当前的检查范围在指定节点的 + * {@link PsiElement#getTextRange()} 范围内; + *
  • + *
  • + * 只能通过 {@link HighlightRangeExtension#isForceHighlightParents} 针对 XLang 文件扩大检查范围, + * 避免子节点的检查错误中断对父节点的检查; + *
  • + *
+ */ @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { if (!(element instanceof XmlElement)) { @@ -50,10 +72,11 @@ public class XLangAnnotator implements Annotator { ProjectEnv.withProject(element.getProject(), () -> { try { - doAnnotate(element, holder); + doAnnotate(holder, element); } catch (Exception e) { - holder.newAnnotation(HighlightSeverity.WARNING, - e.getMessage() != null ? e.getMessage() : e.getClass().getName()) + String msg = e.getMessage() != null ? e.getMessage() : e.getClass().getName(); + + holder.newAnnotation(HighlightSeverity.WARNING, msg) .highlightType(ProblemHighlightType.WARNING) .create(); } @@ -61,52 +84,28 @@ public class XLangAnnotator implements Annotator { }); } - void doAnnotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { - // Note: 在识别引用时,处理的是 XmlText 的 cdata 节点,因此,需要对其子节点做高亮处理 - if (element instanceof XmlText text) { - for (PsiElement child : text.getChildren()) { - doAnnotate(child, holder); - } - return; - } - - checkReferences(element, holder); + private void doAnnotate(@NotNull AnnotationHolder holder, @NotNull PsiElement element) { + checkReferences(holder, element); - if (element instanceof XmlTag) { - XmlTag tag = (XmlTag) element; - - XmlTagInfo tagInfo = getTagInfo(tag); - if (tagInfo == null) { - return; - } - - // 父节点就不合法,则本节点无需处理 - if (tagInfo.getParentDefNode() == null) { - return; - } - - checkTag(tagInfo, holder); + if (element instanceof XLangTag tag) { + checkTag(holder, tag); } // else if (element instanceof XLangAttribute attr) { checkAttr(holder, attr); } // else if (element instanceof XLangAttributeValue attrValue) { - XLangAttribute attr = attrValue.getParentAttr(); - IXDefAttribute attrDef = attr != null ? attr.getDefAttr() : null; - if (attrDef == null) { - return; + checkAttrValue(holder, attrValue); + } + // Note: Annotator 不会触发对 XLangTextToken 等 AST 叶子节点的检查, + // 需通过其父节点(XLangText 等)触发对 XLangTextToken 的检查 + else if (element instanceof XLangText text) { + for (XLangTextToken token : text.getTextTokens()) { + doAnnotate(holder, token); } - - XDefTypeDecl attrType = attrDef.getType(); - checkAttrValue(holder, attr, attrType, attrValue); } } - private XmlTagInfo getTagInfo(PsiElement element) { - return XDefPsiHelper.getTagInfo(element); - } - - private void checkReferences(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + private void checkReferences(@NotNull AnnotationHolder holder, @NotNull PsiElement element) { for (PsiReference reference : element.getReferences()) { if (!(reference instanceof XLangReference ref)) { continue; @@ -114,6 +113,7 @@ public class XLangAnnotator implements Annotator { TextRange textRange = ref.getAbsoluteRange(); PsiElement target = ref.resolve(); + if (target instanceof NopVirtualFile vfs && vfs.forFileChildren()) { holder.newSilentAnnotation(HighlightSeverity.INFORMATION) .range(textRange) @@ -123,45 +123,75 @@ public class XLangAnnotator implements Annotator { String msg = ref.getUnresolvedMessage(); if (target == null && msg != null) { - holder.newAnnotation(HighlightSeverity.ERROR, msg) - .range(textRange) - .highlightType(ProblemHighlightType.ERROR) - .create(); + _errorAnnotation(holder, textRange, msg); } } } - private void checkTag(XmlTagInfo tagInfo, AnnotationHolder holder) { - XmlTag tag = tagInfo.getTag(); - String tagNameStr = tag.getName(); + private void checkTag(@NotNull AnnotationHolder holder, @NotNull XLangTag tag) { + if (tag.getSchemaDefNode() != null) { + checkTagValue(holder, tag); + return; + } - IXDefNode defNode = tagInfo.getDefNode(); - if (defNode == null) { - if (tagInfo.isCustom() || tagInfo.isAllowedUnknownName(tagNameStr)) { - return; - } + XLangTag parentTag = tag.getParentTag(); + if ((parentTag != null && parentTag.isXdefValueSupportBody()) // + || tag.isAllowedUnknownTag() // + ) { + return; + } - XmlToken startTagName = XmlTagUtil.getStartTagNameElement(tag); - holder.newAnnotation(HighlightSeverity.ERROR, - NopPluginBundle.message("xlang.annotation.invalid.tag", tag.getName())) - .range(startTagName) - .highlightType(ProblemHighlightType.ERROR) - .create(); - - XmlToken endTagName = XmlTagUtil.getEndTagNameElement(tag); - if (endTagName != null && endTagName.getText().equals(tagNameStr)) { - holder.newAnnotation(HighlightSeverity.ERROR, - NopPluginBundle.message("xlang.annotation.invalid.tag", tag.getName())) - .range(endTagName) - .highlightType(ProblemHighlightType.ERROR) - .create(); + XmlToken startTagName = XmlTagUtil.getStartTagNameElement(tag); + if (startTagName != null) { + errorAnnotation(holder, + startTagName.getTextRange(), + "xlang.annotation.tag.not-defined", + startTagName.getText()); + } + + XmlToken endTagName = XmlTagUtil.getEndTagNameElement(tag); + if (endTagName != null) { + errorAnnotation(holder, + endTagName.getTextRange(), + "xlang.annotation.tag.not-defined", + endTagName.getText()); + } + } + + private void checkTagValue(@NotNull AnnotationHolder holder, @NotNull XLangTag tag) { + XDefTypeDecl xdefValue = tag.getDefNodeXdefValue(); + TextRange textRange = tag.getValue().getTextRange(); + String bodyText = tag.hasChildTag() ? null : tag.getBodyText(); + boolean blankBodyText = StringHelper.isBlank(bodyText); + + if (xdefValue == null) { + if (!blankBodyText) { + errorAnnotation(holder, textRange, "xlang.annotation.tag.value-not-allowed", tag.getName()); } - } else { - checkValue(holder, tag, defNode); + return; + } + + if (!tag.isAllowedChildTag() && tag.hasChildTag()) { + errorAnnotation(holder, textRange, "xlang.annotation.tag.child-not-allowed", tag.getName()); + return; + } + + if (xdefValue.isMandatory() && blankBodyText) { + errorAnnotation(holder, + getStartTagName(tag).getTextRange(), + "xlang.annotation.tag.body-required", + tag.getName()); + return; + } + + if (!blankBodyText) { + SourceLocation loc = XmlPsiHelper.getValueLocation(tag); + + checkStdDomain(holder, textRange, xdefValue.getStdDomain(), loc, "body", bodyText); } } - private void checkAttr(AnnotationHolder holder, @NotNull XLangAttribute attr) { + private void checkAttr(@NotNull AnnotationHolder holder, @NotNull XLangAttribute attr) { XmlElement attrNameElement = attr.getNameElement(); if (attrNameElement == null // || "xmlns".equals(attrNameElement.getText()) // @@ -171,133 +201,82 @@ public class XLangAnnotator implements Annotator { } if (attr.getDefAttr() == null) { - holder.newAnnotation(HighlightSeverity.ERROR, - NopPluginBundle.message("xlang.annotation.attr.not-defined", attr.getName())) - .range(attrNameElement) - .highlightType(ProblemHighlightType.ERROR) - .create(); + errorAnnotation(holder, + attrNameElement.getTextRange(), + "xlang.annotation.attr.not-defined", + attr.getName()); } } - private void checkAttrValue( - AnnotationHolder holder, XLangAttribute attr, XDefTypeDecl attrType, XLangAttributeValue attrValue - ) { + private void checkAttrValue(@NotNull AnnotationHolder holder, @NotNull XLangAttributeValue attrValue) { + XLangAttribute attr = attrValue.getParentAttr(); + IXDefAttribute attrDef = attr != null ? attr.getDefAttr() : null; + if (attrDef == null) { + return; + } + String attrName = attr.getName(); String attrValueText = attrValue.getValue(); TextRange attrValueTextRange = attrValue.getValueTextRange(); + XDefTypeDecl attrType = attrDef.getType(); if (StringHelper.isEmpty(attrValueText)) { if (attrType.isMandatory()) { - holder.newAnnotation(HighlightSeverity.ERROR, - NopPluginBundle.message("xlang.annotation.attr.value-required", attrName)) - .range(attrValue.getTextRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); + errorAnnotation(holder, attrValue.getTextRange(), "xlang.annotation.attr.value-required", attrName); } return; } // Note: dict/enum 的有效值检查由 PsiReference 处理 - attrValueText = StringHelper.unescapeXml(attrValueText); + SourceLocation loc = XmlPsiHelper.getLocation(attrValue); + checkStdDomain(holder, attrValueTextRange, attrType.getStdDomain(), loc, attrName, attrValueText); + } - IStdDomainHandler domainHandler = StdDomainRegistry.instance().getStdDomainHandler(attrType.getStdDomain()); + private void checkStdDomain( + AnnotationHolder holder, TextRange textRange, // + String stdDomain, SourceLocation loc, String propName, String propValue + ) { + IStdDomainHandler domainHandler = StdDomainRegistry.instance().getStdDomainHandler(stdDomain); if (domainHandler == null) { return; } + propValue = StringHelper.unescapeXml(propValue); + try { - domainHandler.validate(XmlPsiHelper.getLocation(attrValue), - attrName, - attrValueText, - IValidationErrorCollector.THROW_ERROR); + domainHandler.validate(loc, propName, propValue, IValidationErrorCollector.THROW_ERROR); } catch (Exception e) { - String msg = ErrorMessageManager.instance() - .buildErrorMessage(AppConfig.defaultLocale(), e, false, false, false) - .getDescription(); - if (msg == null) { - msg = e.getMessage(); - } - - holder.newAnnotation(HighlightSeverity.ERROR, msg) - .range(attrValueTextRange) - .highlightType(ProblemHighlightType.ERROR) - .create(); + errorAnnotation(holder, textRange, e); } } - private void checkValue(AnnotationHolder holder, XmlTag tag, IXDefNode defNode) { - - String tagValue = getTagValue(tag); - - if (defNode.getXdefValue() != null) { - if (!defNode.hasChild() && !defNode.getXdefValue().isSupportBody(StdDomainRegistry.instance())) { - // 不允许子节点 - if (XmlPsiHelper.hasChild(tag)) { - holder.newAnnotation(HighlightSeverity.ERROR, - NopPluginBundle.message("xlang.annotation.tag.not-allow-child", tag.getName())) - .range(tag.getValue().getTextRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); - return; - } - } - if (defNode.getXdefValue().isMandatory()) { - if (StringHelper.isBlank(tagValue)) { - holder.newAnnotation(HighlightSeverity.ERROR, - NopPluginBundle.message("xlang.annotation.value.not-allow-empty", - tag.getName())) - .range(getStartTagName(tag)) - .highlightType(ProblemHighlightType.ERROR) - .create(); - } - } - if (!StringHelper.isBlank(tagValue)) { - String domain = defNode.getXdefValue().getStdDomain(); - IStdDomainHandler domainHandler = StdDomainRegistry.instance().getStdDomainHandler(domain); - if (domainHandler != null) { - try { - domainHandler.validate(XmlPsiHelper.getValueLocation(tag), - "body", - tagValue, - IValidationErrorCollector.THROW_ERROR); - } catch (Exception e) { - holder.newAnnotation(HighlightSeverity.ERROR, - "err:" + ErrorMessageManager.instance() - .buildErrorMessage(AppConfig.defaultLocale(), - e, - false, - false, - false) - .getDescription()) - .range(tag.getValue().getTextRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); - } - } - } - } else { - if (!StringHelper.isBlank(tagValue)) { - holder.newAnnotation(HighlightSeverity.ERROR, - NopPluginBundle.message("xlang.annotation.tag.not-allow-value", tag.getName())) - .range(tag.getTextRange()) - .highlightType(ProblemHighlightType.ERROR) - .create(); - } - } + private XmlElement getStartTagName(XmlTag tag) { + XmlElement element = XmlTagUtil.getStartTagNameElement(tag); + + return element != null ? element : tag; } - String getTagValue(XmlTag tag) { - if (XmlPsiHelper.hasChild(tag)) { - return null; - } - return StringHelper.unescapeXml(tag.getValue().getText()); + private void errorAnnotation(AnnotationHolder holder, TextRange textRange, String msgKey, Object... msgParams) { + String msg = NopPluginBundle.message(msgKey, msgParams); + + _errorAnnotation(holder, textRange, msg); } - XmlElement getStartTagName(XmlTag tag) { - XmlElement element = XmlTagUtil.getStartTagNameElement(tag); - if (element == null) { - element = tag; + private void errorAnnotation(AnnotationHolder holder, TextRange textRange, Exception e) { + String msg = ErrorMessageManager.instance() + .buildErrorMessage(AppConfig.defaultLocale(), e, false, false, false) + .getDescription(); + if (msg == null) { + msg = e.getMessage(); } - return element; + + _errorAnnotation(holder, textRange, msg); + } + + private void _errorAnnotation(AnnotationHolder holder, TextRange textRange, String msg) { + holder.newAnnotation(HighlightSeverity.ERROR, msg) + .range(textRange) + .highlightType(ProblemHighlightType.ERROR) + .create(); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangHighlightRangeExtension.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangHighlightRangeExtension.java new file mode 100644 index 000000000..e4557a191 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangHighlightRangeExtension.java @@ -0,0 +1,20 @@ +package io.nop.idea.plugin.lang.annotator; + +import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension; +import com.intellij.psi.PsiFile; +import io.nop.idea.plugin.lang.XLangLanguage; +import org.jetbrains.annotations.NotNull; + +/** + * 确保不中断对父节点的 {@link XLangAnnotator} 检查 + * + * @author flytreeleft + * @date 2025-07-16 + */ +public class XLangHighlightRangeExtension implements HighlightRangeExtension { + + @Override + public boolean isForceHighlightParents(@NotNull final PsiFile file) { + return file.getLanguage() == XLangLanguage.INSTANCE; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index 3b1ac3c7a..9e6a54d5c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -16,7 +16,15 @@ import io.nop.xlang.xdsl.XDslKeys; import org.jetbrains.annotations.NotNull; /** - * 属性值,由引号和 {@link XLangValueToken} 组成 + * 属性值,由引号和 {@link XLangValueToken} 组成: + *
+ * XLangAttributeValue
+ *   XmlToken:XML_ATTRIBUTE_VALUE_START_DELIMITER('"')
+ *   XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN('Child ')
+ *   XmlToken:XML_CHAR_ENTITY_REF('&amp;')
+ *   XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN(' Tag')
+ *   XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"')
+ * 
* * @author flytreeleft * @date 2025-07-09 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index a04e785b8..018295f59 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -18,6 +18,7 @@ import io.nop.xlang.xdef.IXDefinition; import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefKeys; import io.nop.xlang.xdef.XDefTypeDecl; +import io.nop.xlang.xdef.domain.StdDomainRegistry; import io.nop.xlang.xdef.impl.XDefAttribute; import io.nop.xlang.xdef.parse.XDefTypeDeclParser; import io.nop.xlang.xdsl.XDslConstants; @@ -25,6 +26,9 @@ import io.nop.xlang.xdsl.XDslKeys; import io.nop.xlang.xpl.xlib.XlibConstants; import org.jetbrains.annotations.NotNull; +import static com.intellij.psi.xml.XmlElementType.XML_TAG; +import static com.intellij.psi.xml.XmlElementType.XML_TEXT; + /** * {@link XNode} 标签(其名字含名字空间) *

@@ -44,6 +48,13 @@ public class XLangTag extends XmlTagImpl { return getClass().getSimpleName() + ':' + getElementType() + "('" + getName() + "')"; } + @Override + public void clearCaches() { + this.schemaMeta = null; + + super.clearCaches(); + } + @Override public XLangTag getParentTag() { return (XLangTag) super.getParentTag(); @@ -61,11 +72,16 @@ public class XLangTag extends XmlTagImpl { } while (true); } - @Override - public void clearCaches() { - this.schemaMeta = null; + /** 当前标签是否有子标签 */ + public boolean hasChildTag() { + return getNode().findChildByType(XML_TAG) != null; + } - super.clearCaches(); + /** 获取当前标签内的文本内容(特殊符号已转义) */ + public @NotNull String getBodyText() { + XLangText text = (XLangText) findPsiChildByType(XML_TEXT); + + return text != null ? text.getTextChars() : ""; } @Override @@ -160,11 +176,6 @@ public class XLangTag extends XmlTagImpl { return getSchemaMeta().selfDefNode; } - /** 获取 {@link #getSelfDefNode()} 上指定的属性 */ - public IXDefAttribute getSelfDefNodeAttr(String attrName) { - return getXDefNodeAttr(getSelfDefNode(), attrName); - } - /** @see SchemaMeta#xdefKeys */ public XDefKeys getXDefKeys() { return getSchemaMeta().xdefKeys; @@ -197,6 +208,45 @@ public class XLangTag extends XmlTagImpl { && !XDslKeys.DEFAULT.equals(xdslKeys); } + /** 当前标签是否允许拥有子标签 */ + public boolean isAllowedChildTag() { + IXDefNode defNode = getSchemaDefNode(); + if (defNode == null) { + return false; + } else if (defNode.hasChild()) { + return true; + } + + return isXdefValueSupportBody(); + } + + /** 当前标签的 {@link #getDefNodeXdefValue() xdef:value} 类型是否支持内嵌节点 */ + public boolean isXdefValueSupportBody() { + XDefTypeDecl xdefValue = getDefNodeXdefValue(); + + return xdefValue != null && xdefValue.isSupportBody(StdDomainRegistry.instance()); + } + + /** + * 当前标签是否为可被允许的未知标签 + *

+ * 仅当前标签包含自定义名字空间,且 {@link #getSchemaDef()} 的 + * {@link IXDefinition#getXdefCheckNs() xdef:check-ns} + * 不包含该名字空间时,该标签才是被许可的 + */ + public boolean isAllowedUnknownTag() { + String name = getName(); + IXDefinition def = getSchemaDef(); + String ns = StringHelper.getNamespace(name); + + if (def == null || ns == null) { + return false; + } + + return def.getXdefCheckNs() == null // + || !def.getXdefCheckNs().contains(ns); + } + /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ private IXDefAttribute getXDefNodeAttr(IXDefNode xdefNode, String attrName) { String xmlnsPrefix = "xmlns:"; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java index dea56066f..8dc42abde 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java @@ -1,13 +1,45 @@ package io.nop.idea.plugin.lang.psi; +import java.util.ArrayList; +import java.util.List; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; import com.intellij.psi.impl.source.xml.XmlTextImpl; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.psi.xml.XmlElementType.XML_CDATA; +import static com.intellij.psi.xml.XmlTokenType.XML_DATA_CHARACTERS; /** * 节点中的文本节点 *

* 包含 CDATA 节点({@link com.intellij.psi.xml.XmlElementType#XML_CDATA XML_CDATA}), * 并且,除了 CDATA 的文本是一个整体外,其余的文本会被拆分为空白和非空白两类 Token 作为 - * {@link XLangText} 的直接叶子节点 + * {@link XLangText} 的直接叶子节点: + *

+ * XLangText:XML_TEXT
+ *   PsiElement(XML_CDATA)
+ *     XmlToken:XML_CDATA_START('')
+ * 
+ *
+ * XLangText:XML_TEXT
+ *   PsiWhiteSpace('\n        ')
+ *   XLangTextToken:XML_DATA_CHARACTERS('This')
+ *   PsiWhiteSpace(' ')
+ *   XLangTextToken:XML_DATA_CHARACTERS('is')
+ *   PsiWhiteSpace(' ')
+ *   XLangTextToken:XML_DATA_CHARACTERS('a')
+ *   PsiWhiteSpace(' ')
+ *   XmlToken:XML_CHAR_ENTITY_REF('&lt;')
+ *   XLangTextToken:XML_DATA_CHARACTERS('text/')
+ *   XmlToken:XML_CHAR_ENTITY_REF('&gt;')
+ *   PsiWhiteSpace(' ')
+ *   XLangTextToken:XML_DATA_CHARACTERS('node.')
+ *   PsiWhiteSpace('\n        ')
+ * 
* * @author flytreeleft * @date 2025-07-09 @@ -16,6 +48,35 @@ public class XLangText extends XmlTextImpl { @Override public String toString() { - return getClass().getSimpleName(); + return getClass().getSimpleName() + ':' + getElementType(); + } + + /** 获取文本内容(特殊符号已转义) */ + public @NotNull String getTextChars() { + PsiElement cdata = findPsiChildByType(XML_CDATA); + + if (cdata != null) { + ASTNode node = cdata.getNode().findChildByType(XML_DATA_CHARACTERS); + return node != null ? node.getText() : ""; + } + return getText(); + } + + public @NotNull XLangTextToken[] getTextTokens() { + List tokens = new ArrayList<>(); + + for (PsiElement child : getChildren()) { + if (child.getNode().getElementType() == XML_CDATA) { + for (PsiElement c : child.getChildren()) { + if (c instanceof XLangTextToken token) { + tokens.add(token); + } + } + } // + else if (child instanceof XLangTextToken token) { + tokens.add(token); + } + } + return tokens.toArray(new XLangTextToken[0]); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java index 179783705..d3bd4e1d1 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java @@ -12,7 +12,22 @@ import org.jetbrains.annotations.NotNull; /** * CDATA 节点({@link com.intellij.psi.xml.XmlElementType#XML_CDATA XML_CDATA})中的全部文本, - * 或者 {@link XLangText} 节点中的非空白文本 + * 或者 {@link XLangText} 节点中的非空白文本: + *
+ * XLangText:XML_TEXT
+ *   PsiElement(XML_CDATA)
+ *     XmlToken:XML_CDATA_START('')
+ * 
+ *
+ * XLangText:XML_TEXT
+ *   PsiWhiteSpace('\n        ')
+ *   XLangTextToken:XML_DATA_CHARACTERS('abc')
+ *   PsiWhiteSpace('\n        ')
+ *   XLangTextToken:XML_DATA_CHARACTERS('def')
+ *   PsiWhiteSpace('\n    ')
+ * 
* * @author flytreeleft * @date 2025-07-09 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java index 86f55003b..1d5d8dd2b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java @@ -5,7 +5,13 @@ import com.intellij.psi.tree.IElementType; import org.jetbrains.annotations.NotNull; /** - * {@link XLangAttributeValue} 中不含引号的部分 + * {@link XLangAttributeValue} 中不含引号的部分: + *
+ * XLangAttributeValue
+ *   XmlToken:XML_ATTRIBUTE_VALUE_START_DELIMITER('"')
+ *   XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN('/nop/schema/xdsl.xdef')
+ *   XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"')
+ * 
* * @author flytreeleft * @date 2025-07-09 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index f394f225a..260c3d3f6 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -240,16 +240,6 @@ public class XmlPsiHelper { return SourceLocation.fromLine(path, sourceLine, sourceColumn, len); } - private static PsiElement getRootElement(PsiElement element) { - do { - PsiElement parent = element.getParent(); - if (parent == null) { - return element; - } - element = parent; - } while (true); - } - public static String getAttrName(XmlAttributeValue value) { if (value == null) { return null; @@ -263,23 +253,6 @@ public class XmlPsiHelper { return attr.getName(); } - public static XmlAttribute getAttr(XmlAttributeValue value) { - if (value == null) { - return null; - } - - if (!(value.getParent() instanceof XmlAttribute)) { - return null; - } - - XmlAttribute attr = (XmlAttribute) value.getParent(); - return attr; - } - - public static boolean hasChild(XmlTag tag) { - return tag.getNode().findChildByType(XmlElementType.XML_TAG) != null; - } - public static boolean isInComment(PsiElement element) { IElementType elementType = element.getNode().getElementType(); return elementType == XmlTokenType.XML_COMMENT_END diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 1b7fe5dd4..c4ff8773b 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -87,7 +87,8 @@ implementationClass="io.nop.idea.plugin.lang.XLangParserDefinition"/> + implementationClass="io.nop.idea.plugin.lang.annotator.XLangAnnotator"/> + @@ -106,11 +107,11 @@ - + extensions="xlangscript"/>--> - + /nop/schema/xdef.xdef,/nop/schema/xdsl.xdef /nop/schema/xdef.xdef, @@ -24,7 +24,7 @@ public class TestXLangParser extends BaseXLangPluginTestCase { This is CDATA text. ]]> - This is a text. + This is a <text/> node. This is a child tag. diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast index 30f8cf35f..08a6aa6f8 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast @@ -23,7 +23,7 @@ XmlFile:unit.xtest XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"') PsiWhiteSpace('\n') XmlToken:XML_TAG_END('>') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTag:XML_TAG('tag') XmlToken:XML_START_TAG_START('<') @@ -34,27 +34,29 @@ XmlFile:unit.xtest XmlToken:XML_EQ('=') XLangAttributeValue XmlToken:XML_ATTRIBUTE_VALUE_START_DELIMITER('"') - XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN('Child') + XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN('Child ') + XmlToken:XML_CHAR_ENTITY_REF('&') + XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN(' Tag') XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"') XmlToken:XML_EMPTY_ELEMENT_END('/>') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTag:XML_TAG('refs') XmlToken:XML_START_TAG_START('<') XmlToken:XML_NAME('refs') XmlToken:XML_TAG_END('>') - XLangText + XLangText:XML_TEXT XLangTextToken:XML_DATA_CHARACTERS('/nop/schema/xdef.xdef,/nop/schema/xdsl.xdef') XmlToken:XML_END_TAG_START('') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTag:XML_TAG('refs') XmlToken:XML_START_TAG_START('<') XmlToken:XML_NAME('refs') XmlToken:XML_TAG_END('>') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTextToken:XML_DATA_CHARACTERS('/nop/schema/xdef.xdef,') PsiWhiteSpace('\n ') @@ -63,13 +65,13 @@ XmlFile:unit.xtest XmlToken:XML_END_TAG_START('') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTag:XML_TAG('text') XmlToken:XML_START_TAG_START('<') XmlToken:XML_NAME('text') XmlToken:XML_TAG_END('>') - XLangText + XLangText:XML_TEXT PsiElement(XML_CDATA) XmlToken:XML_CDATA_START('') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTag:XML_TAG('mix') XmlToken:XML_START_TAG_START('<') XmlToken:XML_NAME('mix') XmlToken:XML_TAG_END('>') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTextToken:XML_DATA_CHARACTERS('This') PsiWhiteSpace(' ') @@ -91,13 +93,17 @@ XmlFile:unit.xtest PsiWhiteSpace(' ') XLangTextToken:XML_DATA_CHARACTERS('a') PsiWhiteSpace(' ') - XLangTextToken:XML_DATA_CHARACTERS('text.') + XmlToken:XML_CHAR_ENTITY_REF('<') + XLangTextToken:XML_DATA_CHARACTERS('text/') + XmlToken:XML_CHAR_ENTITY_REF('>') + PsiWhiteSpace(' ') + XLangTextToken:XML_DATA_CHARACTERS('node.') PsiWhiteSpace('\n ') XLangTag:XML_TAG('tag') XmlToken:XML_START_TAG_START('<') XmlToken:XML_NAME('tag') XmlToken:XML_TAG_END('>') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTextToken:XML_DATA_CHARACTERS('This') PsiWhiteSpace(' ') @@ -112,13 +118,13 @@ XmlFile:unit.xtest XmlToken:XML_END_TAG_START('') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XLangTag:XML_TAG('tag') XmlToken:XML_START_TAG_START('<') XmlToken:XML_NAME('tag') XmlToken:XML_TAG_END('>') - XLangText + XLangText:XML_TEXT PsiElement(XML_CDATA) XmlToken:XML_CDATA_START('') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n ') XmlToken:XML_END_TAG_START('') - XLangText + XLangText:XML_TEXT PsiWhiteSpace('\n') XmlToken:XML_END_TAG_START(' /xdsl.xdef + /nop/schema/xdsl.xdef ]]> + + + + + + + + + + + + + + + + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index f483069af..0ee7a07d9 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -6,6 +6,7 @@ xmlns:c="c" xmlns:xpl="xpl" x:schema="/nop/schema/xdef.xdef" x:dump="true" xdef:bean-package="io.nop.xlang.xdef.domain" xdef:name="XJsonDomainHandler" + xdef:check-ns="xui" > @@ -22,6 +23,9 @@ + + + - + extensions="xlangscript"/> Date: Thu, 17 Jul 2025 09:48:23 +0800 Subject: [PATCH 60/82] =?UTF-8?q?nop-idea-pugin:=20=E8=8B=A5=20XLang=20?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=E7=9B=AE=E6=A0=87=E4=B8=8E=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E5=85=83=E7=B4=A0=E5=9C=A8=E5=90=8C=E5=90=8D=E7=9A=84?= =?UTF-8?q?=20vfs=20=E5=86=85=EF=BC=8C=E5=88=99=E5=AF=B9=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E5=85=83=E7=B4=A0=E7=9A=84=E6=89=80=E5=9C=A8=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/reference/XLangDefAttrReference.java | 4 +-- .../lang/reference/XLangReferenceHelper.java | 5 +-- .../XLangStdDomainXdefRefReference.java | 4 +-- .../nop/idea/plugin/vfs/NopVirtualFile.java | 32 +++++++++++++------ .../plugin/vfs/NopVirtualFileReference.java | 4 +-- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java index 585f3ed7a..d31b7d49f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java @@ -10,7 +10,6 @@ package io.nop.idea.plugin.lang.reference; import java.util.function.Function; -import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -48,7 +47,6 @@ public class XLangDefAttrReference extends XLangReferenceBase { return null; } - Project project = myElement.getProject(); // Note: SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 String path = loc.getPath().replace("classpath:_vfs", ""); @@ -66,6 +64,6 @@ public class XLangDefAttrReference extends XLangReferenceBase { return target; }; - return new NopVirtualFile(project, path, targetResolver); + return new NopVirtualFile(myElement, path, targetResolver); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java index eaf2d8f46..4872a9221 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -17,7 +17,6 @@ import java.util.Map; import java.util.Objects; import java.util.function.Function; -import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -241,8 +240,6 @@ public class XLangReferenceHelper { public static NopVirtualFile createNopVfsForDict( PsiElement refElement, String dictName, Object dictOptionValue ) { - Project project = refElement.getProject(); - Function targetResolver = // (file) -> XmlPsiHelper.findFirstElement(file, (element) -> { if (element instanceof LeafPsiElement value // @@ -259,7 +256,7 @@ public class XLangReferenceHelper { String path = "/dict/" + dictName + ".dict.yaml"; - return new NopVirtualFile(project, path, dictOptionValue != null ? targetResolver : null); + return new NopVirtualFile(refElement, path, dictOptionValue != null ? targetResolver : null); } public static List getRegisteredStdDomains() { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java index 226edc09e..47d367b0d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java @@ -10,7 +10,6 @@ package io.nop.idea.plugin.lang.reference; import java.util.function.Function; -import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -36,7 +35,6 @@ public class XLangStdDomainXdefRefReference extends XLangReferenceBase { @Override public @Nullable PsiElement resolveInner() { - Project project = myElement.getProject(); // - /nop/schema/xdef.xdef: // - `` // - `` @@ -55,7 +53,7 @@ public class XLangStdDomainXdefRefReference extends XLangReferenceBase { path = XmlPsiHelper.getNopVfsAbsolutePath(path, myElement); - target = new NopVirtualFile(project, path, ref != null ? createTargetResolver(ref) : null); + target = new NopVirtualFile(myElement, path, ref != null ? createTargetResolver(ref) : null); if (((NopVirtualFile) target).hasEmptyChildren()) { target = null; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java index 7e73764d5..05bbb6e72 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java @@ -31,20 +31,27 @@ import org.jetbrains.annotations.Nullable; * @date 2025-07-12 */ public class NopVirtualFile extends PsiElementBase implements PsiNamedElement { - private final Project project; - /** vfs 绝对路径 */ + private final PsiElement srcElement; private final String path; - /** 目标元素获取函数 */ private final Function targetResolver; private PsiElement[] children; - public NopVirtualFile(Project project, String path) { - this(project, path, null); - } - - public NopVirtualFile(Project project, String path, Function targetResolver) { - this.project = project; + /** @see #NopVirtualFile(PsiElement, String, Function) */ + public NopVirtualFile(PsiElement srcElement, String path) { + this(srcElement, path, null); + } + + /** + * @param srcElement + * 与该 vfs 直接相关的源元素 + * @param path + * 该 vfs 的绝对路径 + * @param targetResolver + * 目标元素获取函数 + */ + public NopVirtualFile(PsiElement srcElement, String path, Function targetResolver) { + this.srcElement = srcElement; this.path = path; assert path.startsWith("/"); @@ -101,8 +108,13 @@ public class NopVirtualFile extends PsiElementBase implements PsiNamedElement { @Override public @NotNull PsiElement @NotNull [] getChildren() { if (children == null) { + PsiFile srcFile = srcElement.getContainingFile(); + String srcVfsPath = XmlPsiHelper.getNopVfsPath(srcElement); + children = XmlPsiHelper.findPsiFilesByNopVfsPath(this, path) .stream() + // Note: 如果是同名的 vfs,则仅对同一文件做引用识别 + .filter(file -> !path.equals(srcVfsPath) || srcFile == file) .map(file -> targetResolver != null ? targetResolver.apply(file) : file) .filter(Objects::nonNull) .toArray(PsiElement[]::new); @@ -112,7 +124,7 @@ public class NopVirtualFile extends PsiElementBase implements PsiNamedElement { @Override public @NotNull Project getProject() { - return project; + return srcElement.getProject(); } public String getPath() { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java index 6e30ea59b..dfff36daa 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java @@ -1,6 +1,5 @@ package io.nop.idea.plugin.vfs; -import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import io.nop.idea.plugin.lang.reference.XLangReferenceBase; @@ -29,10 +28,9 @@ public class NopVirtualFileReference extends XLangReferenceBase { @Override public @Nullable PsiElement resolveInner() { - Project project = myElement.getProject(); String absPath = XmlPsiHelper.getNopVfsAbsolutePath(path, myElement); - NopVirtualFile target = new NopVirtualFile(project, absPath); + NopVirtualFile target = new NopVirtualFile(myElement, absPath); if (target.hasEmptyChildren()) { String msg = NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", path); -- Gitee From 3314404bc4f325a2492a8c7c5cdf8766d980b27b Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 17 Jul 2025 15:30:59 +0800 Subject: [PATCH 61/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E8=8A=82=E7=82=B9=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../XLangAnnotator.java | 2 +- .../XLangHighlightRangeExtension.java | 2 +- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 75 ++++-- .../lang/reference/XLangDefAttrReference.java | 6 +- .../lang/reference/XLangReferenceBase.java | 3 + .../lang/reference/XLangReferenceHelper.java | 26 +- .../lang/reference/XLangTagReference.java | 51 ++++ .../nop/idea/plugin/utils/XmlPsiHelper.java | 15 +- .../src/main/resources/META-INF/plugin.xml | 4 +- .../idea/plugin/lang/TestXLangReferences.java | 224 +++++++++++++++++- .../resources/_vfs/test/doc/example-1.xdoc | 15 +- 11 files changed, 365 insertions(+), 58 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/{annotator => highlight}/XLangAnnotator.java (99%) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/{annotator => highlight}/XLangHighlightRangeExtension.java (95%) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangAnnotator.java similarity index 99% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangAnnotator.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangAnnotator.java index cd9ed2231..0cb143949 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangAnnotator.java @@ -5,7 +5,7 @@ * Gitee: https://gitee.com/canonical-entropy/nop-entropy * Github: https://github.com/entropy-cloud/nop-entropy */ -package io.nop.idea.plugin.lang.annotator; +package io.nop.idea.plugin.lang.highlight; import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension; import com.intellij.codeInspection.ProblemHighlightType; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangHighlightRangeExtension.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangHighlightRangeExtension.java similarity index 95% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangHighlightRangeExtension.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangHighlightRangeExtension.java index 5da566c67..0970bda85 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/annotator/XLangHighlightRangeExtension.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangHighlightRangeExtension.java @@ -6,7 +6,7 @@ * Github: https://github.com/entropy-cloud/nop-entropy */ -package io.nop.idea.plugin.lang.annotator; +package io.nop.idea.plugin.lang.highlight; import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension; import com.intellij.psi.PsiFile; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 40451088f..b7f60162c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -8,15 +8,23 @@ package io.nop.idea.plugin.lang.psi; +import java.util.ArrayList; +import java.util.List; + import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; +import com.intellij.psi.impl.source.xml.TagNameReference; import com.intellij.psi.impl.source.xml.XmlTagImpl; +import com.intellij.psi.xml.XmlToken; +import com.intellij.xml.util.XmlTagUtil; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.core.lang.xml.XNode; +import io.nop.idea.plugin.lang.reference.XLangTagReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -94,18 +102,41 @@ public class XLangTag extends XmlTagImpl { @Override public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { - // TODO 通过 CachedValuesManager.getCachedValue 缓存结果,并支持在依赖项变更时丢弃缓存结果 -// if (hints == PsiReferenceService.Hints.NO_HINTS) { -// return CachedValuesManager.getCachedValue(this, -// () -> CachedValueProvider.Result.create(getReferencesImpl( -// PsiReferenceService.Hints.NO_HINTS), -// PsiModificationTracker.MODIFICATION_COUNT, -// externalResourceModificationTracker( -// myTag))).clone(); -// } - - // TODO 合并 xml 与 xlang 的引用 - return super.getReferences(hints); + List refs = new ArrayList<>(); + + // TODO xlib 函数标签的名字空间引用的是 xlib 的文件名字 + // 参考 XmlTagDelegate#getReferencesImpl + PsiReference[] xmlRefs = super.getReferences(hints); + // Note: 仅保留对名字空间的引用,以支持对其做高亮、重命名等 + for (PsiReference ref : xmlRefs) { + if (!(ref instanceof TagNameReference)) { + refs.add(ref); + } + } + + // 对起止标签均做引用识别 + XmlToken[] tagNameTokens = new XmlToken[] { + XmlTagUtil.getStartTagNameElement(this), // + XmlTagUtil.getEndTagNameElement(this) + }; + for (XmlToken token : tagNameTokens) { + if (token == null) { + continue; + } + + String name = token.getText(); + int nsIndex = name.indexOf(':'); + // Note: 针对起止标签名在当前标签中的文本范围创建引用,而不是针对起止标签名自身创建引用 + TextRange textRange = TextRange.allOf(name.substring(nsIndex + 1)) + .shiftRight(token.getStartOffsetInParent() + nsIndex + 1); + XLangTagReference ref = new XLangTagReference(this, textRange); + + // TODO 对 xlib 标签单独做引用识别 + + refs.add(ref); + } + + return refs.toArray(PsiReference.EMPTY_ARRAY); } /** @@ -135,11 +166,11 @@ public class XLangTag extends XmlTagImpl { * 即,二者形成交叉定义 */ public IXDefAttribute getSchemaDefNodeAttr(String attrName) { - // 在元元模型中,以 xdef 为名字空间的属性,需迫使其以 meta:unknown-attr 作为其属性定义 + // 在元元模型中,以 xdef 为名字空间的属性,需以 meta:unknown-attr 作为其属性定义 if (isInXDefXDef() && attrName.startsWith(XDefKeys.DEFAULT.NS + ':')) { attrName = "*"; } - // xdef.xdef 的属性在固定的名字空间 x 中声明 + // xdef.xdef 的属性在固定的名字空间 xdef 中声明 else { attrName = changeNamespace(attrName, getXDefKeys().NS, XDefKeys.DEFAULT.NS); } @@ -385,14 +416,20 @@ public class XLangTag extends XmlTagImpl { IXDefNode schemaDefNode = null; if (tagNs.equals(XDslKeys.DEFAULT.NS) && !parentTag.isInXDslXDef()) { schemaDefNode = xdslDefNode; + selfDefNode = null; } // else if (parentSchemaDefNode != null) { - // Note: 如果是 xdef.xdef 中的节点,则其节点定义均为 xdef:unknown-tag - boolean inXDefXDef = parentTag.isInXDefXDef(); + // 在元元模型中,以 xdef 为名字空间的标签, + // 需以 meta:unknown-tag 作为其节点定义,即,交叉定义 + if (parentTag.isInXDefXDef() && tagNs.equals(XDefKeys.DEFAULT.NS)) { + schemaDefNode = parentSchemaDefNode.getXdefUnknownTag(); + } + // 其余的,则将标签的 xdef 名字空间固定为名字 xdef + else { + tagName = changeNamespace(tagName, xdefKeys.NS, XDefKeys.DEFAULT.NS); - schemaDefNode = inXDefXDef // - ? parentSchemaDefNode.getXdefUnknownTag() // - : parentSchemaDefNode.getChild(tagName); + schemaDefNode = parentSchemaDefNode.getChild(tagName); + } } return new SchemaMeta(schemaDef, schemaDefNode, xdslDefNode, selfDef, selfDefNode, xdefKeys, xdslKeys); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java index d31b7d49f..c2555bb6b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java @@ -43,13 +43,11 @@ public class XLangDefAttrReference extends XLangReferenceBase { } SourceLocation loc = attrDef.getLocation(); - if (loc == null) { + String path = XmlPsiHelper.getNopVfsPath(loc); + if (path == null) { return null; } - // Note: SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 - String path = loc.getPath().replace("classpath:_vfs", ""); - Function targetResolver = (file) -> { PsiElement target = XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java index 8aeb3b2d1..4722264f6 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java @@ -38,6 +38,9 @@ public abstract class XLangReferenceBase extends CachingReference implements XLa * myRangeInElement 为该元素的文本自身,即,[0, 文本长度] *

* 相关处理逻辑见 {@link com.intellij.psi.impl.SharedPsiElementImplUtil#addReferences SharedPsiElementImplUtil#addReferences} + * + * @param myElement + * 需创建当前引用的元素 */ public XLangReferenceBase(PsiElement myElement, TextRange myRangeInElement) { this.myElement = myElement; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java index 4872a9221..89c57575e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -143,24 +143,30 @@ public class XLangReferenceHelper { int offset = textRangeOffset + optionsIndex; textRange = new TextRange(0, options.length()).shiftRight(offset); - if (STD_DOMAIN_ENUM.equals(stdDomain)) { - // Note: 忽略 enum:a|b|c|d 形式的数据 - if (StringHelper.isValidClassName(options)) { - PsiReference[] ref = PsiClassHelper.createJavaClassReferences(refElement, options, offset); + switch (stdDomain) { + case STD_DOMAIN_ENUM -> { + // Note: 忽略 enum:a|b|c|d 形式的数据 + if (StringHelper.isValidClassName(options)) { + PsiReference[] ref = PsiClassHelper.createJavaClassReferences(refElement, options, offset); - Collections.addAll(refs, ref); + Collections.addAll(refs, ref); + } + } + case STD_DOMAIN_DICT -> { + refs.add(new XLangDictOptionReference(refElement, textRange, options)); } - } // - else if (STD_DOMAIN_DICT.equals(stdDomain)) { - refs.add(new XLangDictOptionReference(refElement, textRange, options)); } } - // 引用字典/枚举值 if (defaultValueIndex > 0) { textRange = new TextRange(0, defaultValue.length()).shiftRight(textRangeOffset + defaultValueIndex); - refs.add(new XLangDictOptionReference(refElement, textRange, options, defaultValue)); + // 引用字典项或枚举值 + switch (stdDomain) { + case STD_DOMAIN_ENUM, STD_DOMAIN_DICT -> { + refs.add(new XLangDictOptionReference(refElement, textRange, options, defaultValue)); + } + } } // 引用节点属性 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java new file mode 100644 index 000000000..2c3d42503 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang.reference; + +import java.util.function.Function; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.xml.XmlTag; +import io.nop.api.core.util.SourceLocation; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import io.nop.xlang.xdef.IXDefNode; +import org.jetbrains.annotations.Nullable; + +/** + * 对节点定义的引用:指向节点的定义位置 + * + * @author flytreeleft + * @date 2025-07-17 + */ +public class XLangTagReference extends XLangReferenceBase { + + public XLangTagReference(XLangTag myElement, TextRange myRangeInElement) { + super(myElement, myRangeInElement); + } + + @Override + public @Nullable PsiElement resolveInner() { + XLangTag tag = (XLangTag) myElement; + IXDefNode defNode = tag.getSchemaDefNode(); + + SourceLocation loc = defNode != null ? defNode.getLocation() : null; + String path = XmlPsiHelper.getNopVfsPath(loc); + if (path == null) { + return null; + } + + Function targetResolver = (file) -> XmlPsiHelper.getPsiElementAt(file, loc, XmlTag.class); + + return new NopVirtualFile(myElement, path, targetResolver); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 260c3d3f6..f70b94431 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -43,6 +43,13 @@ import org.jetbrains.annotations.NotNull; public class XmlPsiHelper { + public static String getNopVfsPath(SourceLocation loc) { + String path = loc != null ? loc.getPath() : null; + + // Note: SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 + return path != null ? path.replace("classpath:_vfs", "") : null; + } + public static String getNopVfsPath(PsiElement element) { PsiFile file = element.getContainingFile(); if (file == null) { @@ -89,14 +96,6 @@ public class XmlPsiHelper { return ret; } - public static PsiFile[] findPsiFiles(Project project, String path) { - List list = findPsiFileList(project, path); - if (list.isEmpty()) { - return PsiFile.EMPTY_ARRAY; - } - return list.toArray(PsiFile.EMPTY_ARRAY); - } - public static List findPsiFilesByNopVfsPath(PsiElement element, String path) { Project project = element.getProject(); String absPath = getNopVfsAbsolutePath(path, element); diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 6c886de5b..e09d07b19 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -87,8 +87,8 @@ implementationClass="io.nop.idea.plugin.lang.XLangParserDefinition"/> - + implementationClass="io.nop.idea.plugin.lang.highlight.XLangAnnotator"/> + diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 0137780b6..f490167fa 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -23,7 +23,147 @@ import org.jetbrains.annotations.NotNull; */ public class TestXLangReferences extends BaseXLangPluginTestCase { - public void testTagReferences() { + public void testTagDefReferences() { + // 名字空间保持名字引用 + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("def:pre-parse"), + "xdef"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", "ta:define>"), + "meta"); + + // *.xdef 的根节点定义始终对应 xdef.xdef 的根节点 meta:unknown-tag, + // 同时,在 xdef.xdef 中未定义的子节点也为对应的 meta:unknown-tag + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("own-tag x:schema"), + "/nop/schema/xdef.xdef?meta:unknown-tag"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("nown-tag xdsl:schema"), + "/nop/schema/xdef.xdef?meta:unknown-tag"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("-parse"), + "/nop/schema/xdef.xdef?meta:unknown-tag"); + assertReference(readVfsResource("/nop/schema/xpl.xdef").replace("", + " xdef:value=\"xpl\"/>"), + "/nop/schema/xdef.xdef?meta:unknown-tag"); + assertReference(""" + + mple> + """, "/nop/schema/xdef.xdef?meta:unknown-tag"); + + // xdef.xdef 中的节点交叉定义 + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("parse"), + "/nop/schema/xdef.xdef?meta:unknown-tag"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("n-tag"), + "/nop/schema/xdef.xdef?meta:unknown-tag"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", "fine>"), + "/nop/schema/xdef.xdef?xdef:define"); + assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", + "own-tag meta:ref=\"XDefNode\"/>"), + "/nop/schema/xdef.xdef?xdef:unknown-tag"); + + // 对 xdef.xdef 中同名子节点的定义引用 + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("", + "own-tag xdef:ref=\"DslNode\"/>"), + "/nop/schema/xdef.xdef?xdef:unknown-tag"); + assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("nown-tag xdef:value"), + "/nop/schema/xdef.xdef?xdef:unknown-tag"); + + assertReference(readVfsResource("/nop/schema/xpl.xdef").replace("ine xdef:name"), + "/nop/schema/xdef.xdef?xdef:define"); + assertReference(readVfsResource("/nop/schema/xpl.xdef").replace("own-tag xpl:unknown-attr"), + "/nop/schema/xdef.xdef?xdef:unknown-tag"); + + // 普通 xdef 内的定义引用 + assertReference(""" + + -parse/> + + """, "/nop/schema/xdef.xdef?xdef:post-parse"); + assertReference(""" + + ld name="string"/> + + """, "/nop/schema/xdef.xdef?meta:unknown-tag"); + + // DSL 内的定义引用 + assertReference(""" + mple xmlns:x="/nop/schema/xdsl.xdef" + x:schema="/test/doc/example.xdef" + > + + """, "/test/doc/example.xdef?example"); + assertReference(""" + mple xmlns:x="/nop/schema/xdsl.xdef" + x:schema="/test/doc/example.xdef" + > + extends/> + + """, "/nop/schema/xdsl.xdef?x:gen-extends"); + assertReference(""" + + ild name="Child"/> + + """, "/test/doc/example.xdef?child"); + assertReference(""" + + own name="abc"/> + + """, "/test/doc/example.xdef?xdef:unknown-tag"); + assertReference(""" + + + c name="abc"/> + + + """, null); + assertReference(""" + + + xtends/> + + + """, "/nop/schema/xdsl.xdef?x:gen-extends"); + } + + public void testXplReferences() { + assertReference(""" + + + ort from="/test/reference/a.xlib"/> + + + """, ""); + assertReference(""" + + + ipt> + + + """, ""); + + // 通过 xpl:lib 导入 xlib assertReference(""" """, "/test/reference/a.xlib#DoFindByMdxQuery"); + assertReference(""" + + + hod="post" xpl:lib="/test/reference/a.xlib"/> + + + """, "/test/reference/a.xlib#DoFindByMdxQuery"); + + // 通过 c:import 导入 xlib + assertReference(""" + + + + ByMdxQuery/> + + + """, "/test/reference/a.xlib#DoFindByMdxQuery"); + assertReference(""" + + + + hod="post"/> + + + """, "/test/reference/a.xlib#DoFindByMdxQuery"); + assertReference(""" + + + + ByMdxQuery/> + + + """, "/test/reference/a.xlib#DoFindByMdxQuery"); + assertReference(""" + + + + hod="post"/> + + + """, "/test/reference/a.xlib#DoFindByMdxQuery"); } public void testAttributeReferences() { @@ -79,7 +275,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"XDefNode\""), - "/nop/schema/xdef.xdef?meta:define#xdef:name=var-name"); + "/nop/schema/xdef.xdef?xdef:define#xdef:name=!var-name"); // xdsl.xdef 中的属性定义识别 // - 交叉识别 @@ -271,7 +467,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", "xdef:ref=\"DslNode\""), "xdef:unknown-tag#xdef:name=DslNode"); - // - 引用文件的相对路径出现在开头:单元测试中暂时无法查找 vfs 相对路径 +// // - 引用文件的相对路径出现在开头:单元测试中暂时无法查找 vfs 相对路径 // assertReference(readVfsResource("/nop/schema/xui/simple-component.xdef").replace( // "xdef:ref=\"../xui/import.xdef\"", // "xdef:ref=\"../xui/import.xdef\""), "/nop/schema/xui/import.xdef"); @@ -435,6 +631,13 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:default-override=\"append\"", "xdef:default-override=\"append\""), // "io.nop.xlang.xdef.XDefOverride#APPEND"); + assertReference(readVfsResource("/nop/schema/xlib.xdef").replace("macro=\"!boolean=false\"", + "macro=\"!boolean=false\""), // + "/dict/core/std-domain.dict.yaml#boolean"); + assertReference(readVfsResource("/nop/schema/xlib.xdef").replace("macro=\"!boolean=false\"", + "macro=\"!boolean=false\""), // + null); + assertReference(""" { - if (loc == null) { - // 尝试取 xdef:unknown-attr - SourceLocation sl = tagInfo.getDefNode().getLocation(); - XmlTag tag = XmlPsiHelper.getPsiElementAt(file, sl, XmlTag.class); - - return tag != null ? tag.getAttribute("xdef:unknown-attr") : null; - } else { - return XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class); - } - }) - .filter(Objects::nonNull) - .map((defAttr) -> new XLangXDefReference(attr, textRange, defAttr)) - .toArray(PsiReference[]::new); - if (refs.length > 0) { - return refs; - } - - String msg = NopPluginBundle.message("xlang.annotation.reference.attr-xdef-not-defined", attrName); - return new PsiReference[] { - new XLangNotFoundReference(attr, textRange, msg) - }; - } - - /** 获取 xml 属性值对应的引用(文件、节点、属性类型等) */ - private PsiReference @NotNull [] getReferencesFromXmlAttributeValue( - XmlAttributeValue refElement, XmlAttribute attr - ) { - // Note: XmlAttributeValue#getValue 的结果包含引号 - String attrValue = attr.getValue(); - if (StringHelper.isEmpty(attrValue)) { - return PsiReference.EMPTY_ARRAY; - } - - String attrName = attr.getName(); - - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); - XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getDefAttrType(attrName) : null; - // 在无节点定义时,则做默认识别 - if (attrDefType == null) { - return getReferencesByDefault(refElement, attrValue); - } - - // 对于声明属性,仅对其类型的定义(涉及枚举和字典)做引用识别 - if (tagInfo.isDefDeclaredAttr(attrName)) { - return getReferencesByDefDeclaredAttr(refElement, attrValue); - } - - // 根据属性声明的类型,对属性值做文件/名字引用 - PsiReference[] refs = getReferencesByDefType(refElement, attrValue, attrDefType); - if (refs != null) { - return refs; - } - - String xdslNs = XDefPsiHelper.getXDslNamespace(tagInfo.getTag()); - if ((xdslNs + ":prototype").equals(attrName)) { - return getReferencesFromPrototype(refElement, attrValue, tagInfo); - } else { - String xdefNs = XDefPsiHelper.getXDefNamespace(tagInfo.getTag()); - - if ((xdefNs + ":key-attr").equals(attrName)) { - return getReferencesFromKeyAttr(refElement, attrValue, tagInfo); - } else if ((xdefNs + ":unique-attr").equals(attrName)) { - return getReferencesFromUniqueAttr(refElement, attrValue, tagInfo); - } - } - - // TODO 其他引用识别 - // - // - - return getReferencesByDefault(refElement, attrValue); - } - - private PsiReference[] getReferencesFromXmlText(XmlElement refElement, XmlTag tag) { - XmlTagInfo tagInfo = tag != null ? XDefPsiHelper.getTagInfo(tag) : null; - if (tagInfo == null) { - return PsiReference.EMPTY_ARRAY; - } - - XDefTypeDecl tagDefType = tagInfo.getDefNode().getXdefValue(); - if (tagDefType == null) { - return PsiReference.EMPTY_ARRAY; - } - - String refValue = refElement.getText(); - PsiReference[] refs = getReferencesByDefType(refElement, refValue, tagDefType); - if (refs != null) { - return refs; - } - - return getReferencesByDefault(refElement, refValue); - } - - /** - * 根据数据域类型识别引用 - * - * @return 若返回 null,则表示未支持对指定类型的处理 - */ - private PsiReference[] getReferencesByDefType( - XmlElement refElement, String refValue, XDefTypeDecl refDefType - ) { - // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, - // 从而精确匹配与引用相关的文本内容 - int textRangeOffset = refElement.getText().indexOf(refValue); - TextRange textRange = new TextRange(0, refValue.length()).shiftRight(textRangeOffset); - - String stdDomain = refDefType.getStdDomain(); - - if (XDefConstants.STD_DOMAIN_V_PATH.equals(stdDomain) // - || XDefConstants.STD_DOMAIN_NAME_OR_V_PATH.equals(stdDomain) // - ) { - return getReferencesByVfsPath(refElement, refValue, textRange); - } // - else if (XDefConstants.STD_DOMAIN_V_PATH_LIST.equals(stdDomain)) { - return getReferencesFromVfsPathCsv(refElement, refValue, textRangeOffset); - } // - else if (XDefConstants.STD_DOMAIN_XDEF_REF.equals(stdDomain)) { - return getReferencesFromXDefRef(refElement, refValue, textRange); - } - - return null; - } - - /** 根据属性的定义识别引用 */ - private PsiReference[] getReferencesByDefDeclaredAttr(XmlElement refElement, String refValue) { - XDefTypeDecl refDefType = new XDefTypeDeclParser().parseFromText(null, refValue); - - // (!~#)?{stdDomain}(:{options})?(={defaultValue})? - String stdDomain = refDefType.getStdDomain(); - String options = refDefType.getOptions(); - Object defaultValue = refDefType.getDefaultValue(); - List defaultAttrNames = refDefType.getDefaultAttrNames(); - - // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, - // 从而精确匹配与引用相关的文本内容 - int textRangeOffset = refElement.getText().indexOf(refValue); - - int stdDomainIndex = refValue.indexOf(stdDomain); - int optionsIndex = options != null ? refValue.indexOf(XDEF_TYPE_PREFIX_OPTIONS + options) + 1 : -1; - int defaultValueIndex = defaultValue != null ? refValue.indexOf("=" + defaultValue) + 1 : -1; - int defaultAttrNamesIndex = defaultAttrNames != null ? refValue.indexOf('=' + XDEF_TYPE_ATTR_PREFIX) - + 1 - + XDEF_TYPE_ATTR_PREFIX.length() : -1; - - List refs = new ArrayList<>(); - - // 引用数据域的类型定义 - TextRange textRange = new TextRange(0, stdDomain.length()).shiftRight(textRangeOffset + stdDomainIndex); - refs.addAll(getReferencesFromDictYaml(refElement, "core/std-domain", stdDomain, textRange)); - - if (optionsIndex > 0) { - textRange = new TextRange(0, options.length()).shiftRight(textRangeOffset + optionsIndex); - - if (STD_DOMAIN_ENUM.equals(stdDomain)) { - if (StringHelper.isValidClassName(options)) { - PsiElement target = JavaPsiFacade.getInstance(refElement.getProject()) - .findClass(options, - GlobalSearchScope.allScope(refElement.getProject())); - - refs.add(new XLangElementReference(refElement, textRange, target)); - } - } else if (STD_DOMAIN_DICT.equals(stdDomain)) { - List files = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, - "/dict/" + options + ".dict.yaml"); - - for (PsiFile file : files) { - refs.add(new XLangVfsFileReference(refElement, textRange, file)); - } - } - } - - // 引用字典/枚举值 - if (defaultValueIndex > 0) { - textRange = new TextRange(0, defaultValue.toString().length()).shiftRight(textRangeOffset - + defaultValueIndex); - - DictBean dictBean = DictProvider.instance().getDict(null, options, null, null); - DictOptionBean dictOpt = dictBean != null ? dictBean.getOptionByValue(defaultValue) : null; - - if (dictOpt instanceof EnumDictOptionBean opt) { - refs.add(new XLangElementReference(refElement, textRange, opt.target)); - } else if (STD_DOMAIN_DICT.equals(stdDomain)) { - refs.addAll(getReferencesFromDictYaml(refElement, options, defaultValue, textRange)); - } - } - - // 引用节点属性 - if (defaultAttrNamesIndex > 0) { - XmlTag tag = PsiTreeUtil.getParentOfType(refElement, XmlTag.class); - Map rangeNameMap = extractValuesFromCsv(refValue.substring(defaultAttrNamesIndex)); - - rangeNameMap.forEach((range, name) -> { - XmlAttribute attr = tag.getAttribute(name); - - range = range.shiftRight(textRangeOffset + defaultAttrNamesIndex); - if (attr == null) { - String msg = NopPluginBundle.message("xlang.annotation.reference.default-value-ref-attr-not-found", - name); - - refs.add(new XLangNotFoundReference(refElement, range, msg)); - } else { - refs.add(new XLangElementReference(refElement, range, attr)); - } - }); - } - - return refs.toArray(PsiReference[]::new); - } - - /** 对文本做默认的引用识别 */ - private PsiReference[] getReferencesByDefault(XmlElement refElement, String refValue) { - if (!refValue.endsWith(".xdef")) { - return PsiReference.EMPTY_ARRAY; - } - - // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, - // 从而精确匹配与引用相关的文本内容 - int textRangeOffset = refElement.getText().indexOf(refValue); - TextRange textRange = new TextRange(0, refValue.length()).shiftRight(textRangeOffset); - - return getReferencesByVfsPath(refElement, refValue, textRange); - } - - /** 从 csv 文本中获取引用 */ - private PsiReference[] getReferencesFromVfsPathCsv( - XmlElement refElement, String refValue, int textRangeOffset - ) { - Map rangePathMap = extractValuesFromCsv(refValue); - - List list = new ArrayList<>(rangePathMap.size()); - rangePathMap.forEach((textRange, path) -> { - PsiReference[] refs = getReferencesByVfsPath(refElement, path, textRange.shiftRight(textRangeOffset)); - - list.addAll(Arrays.stream(refs).toList()); - }); - - return list.toArray(PsiReference[]::new); - } - - /** 从 xdef-ref 类型的属性值中获取引用 */ - private PsiReference[] getReferencesFromXDefRef(XmlElement refElement, String refValue, TextRange textRange) { - // - /nop/schema/xdef.xdef: - // - `` - // - `` - // - /nop/schema/schema/schema-node.xdef: - // `` - - String ref; - String path = null; - List psiFiles; - // 含有后缀的,视为文件引用:相对路径 .. 可能出现在开头,故而,检查最后一个 . 的位置 - if (refValue.lastIndexOf('.') > 0) { - int hashIndex = refValue.indexOf('#'); - - path = hashIndex > 0 ? refValue.substring(0, hashIndex) : refValue; - ref = hashIndex > 0 ? refValue.substring(hashIndex + 1) : null; - - // 文件引用直接返回 - if (ref == null) { - return getReferencesByVfsPath(refElement, path, textRange); - } - - psiFiles = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, path); - } - // 否则,视为名字引用 - else { - ref = refValue; - // Note: 只能引用当前文件(不一定是 vfs)内的名字 - psiFiles = Collections.singletonList(refElement.getContainingFile()); - } - - // 收集引用节点属性 - PsiReference[] refs = psiFiles.stream() - .map((file) -> (XmlAttribute) XmlPsiHelper.findFirstElement(file, (element) -> { - if (element instanceof XmlAttribute attr) { - String name = attr.getName(); - String value = attr.getValue(); - - // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 - return ("xdef:name".equals(name) // - || "meta:name".equals(name) // - ) && ref.equals(value); - } - return false; - })) - .filter(Objects::nonNull) - .map((attr) -> new XLangElementReference(refElement, textRange, attr)) - .toArray(PsiReference[]::new); - if (refs.length > 0) { - return refs; - } - - String msg = path == null - ? NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found", ref) - : NopPluginBundle.message("xlang.annotation.reference.xdef-ref-not-found-in-path", ref, path); - return new PsiReference[] { - new XLangNotFoundReference(refElement, textRange, msg) - }; - } - - /** 从 x:prototype 的属性值中获取引用 */ - private PsiReference[] getReferencesFromPrototype( - XmlAttributeValue attrValueElement, String attrValue, XmlTagInfo tagInfo - ) { - // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, attrValue.length() + 1); - - // 仅从父节点中取引用到的子节点 - // io.nop.xlang.delta.DeltaMerger#mergePrototype - IXDefNode defNode = tagInfo.getDefNode(); - IXDefNode parentDefNode = tagInfo.getParentDefNode(); - - String keyAttr = parentDefNode.getXdefKeyAttr(); - if (keyAttr == null) { - keyAttr = defNode.getXdefUniqueAttr(); - } - - XmlTag parentTag = tagInfo.getTag().getParentTag(); - if (parentTag == null) { - String msg = NopPluginBundle.message("xlang.annotation.reference.x-prototype-no-parent"); - return new PsiReference[] { - new XLangNotFoundReference(attrValueElement, textRange, msg) - }; - } - - XmlTag protoTag = XmlPsiHelper.getChildTagByAttr(parentTag, keyAttr, attrValue); - if (protoTag == null) { - String msg = keyAttr == null - ? NopPluginBundle.message("xlang.annotation.reference.x-prototype-tag-not-found", - attrValue) - : NopPluginBundle.message("xlang.annotation.reference.x-prototype-attr-not-found", - keyAttr, - attrValue); - return new PsiReference[] { - new XLangNotFoundReference(attrValueElement, textRange, msg) - }; - } - - // 定位到目标属性或标签上 - XmlElement target = keyAttr != null ? protoTag.getAttribute(keyAttr) : protoTag; - - return new PsiReference[] { new XLangElementReference(attrValueElement, textRange, target) }; - } - - /** 从 xdef:key-attr 的属性值中获取引用 */ - private PsiReference[] getReferencesFromKeyAttr( - XmlAttributeValue attrValueElement, String attrValue, XmlTagInfo tagInfo - ) { - // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, attrValue.length() + 1); - - PsiReference[] refs = XmlPsiHelper.getAttrsFromChildTag(tagInfo.getTag(), attrValue) - .stream() - .map((attr) -> new XLangElementReference(attrValueElement, textRange, attr)) - .toArray(PsiReference[]::new); - if (refs.length > 0) { - return refs; - } - - String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-key-attr-not-found", attrValue); - return new PsiReference[] { - new XLangNotFoundReference(attrValueElement, textRange, msg) - }; - } - - /** 从 xdef:unique-attr 的属性值中获取引用 */ - private PsiReference[] getReferencesFromUniqueAttr( - XmlAttributeValue attrValueElement, String attrValue, XmlTagInfo tagInfo - ) { - // Note: XmlAttributeValue 的文本范围是包含引号的 - TextRange textRange = new TextRange(1, attrValue.length() + 1); - - // 仅从当前节点中取引用到的属性 - XmlTag tag = tagInfo.getTag(); - XmlAttribute attr = tag.getAttribute(attrValue); - if (attr == null) { - String msg = NopPluginBundle.message("xlang.annotation.reference.xdef-unique-attr-not-found", attrValue); - return new PsiReference[] { - new XLangNotFoundReference(attrValueElement, textRange, msg) - }; - } - - return new PsiReference[] { new XLangElementReference(attrValueElement, textRange, attr) }; - } - - private PsiReference[] getReferencesByVfsPath(XmlElement refElement, String path, TextRange textRange) { - if (!StringHelper.isValidFilePath(path) || path.lastIndexOf('.') <= 0) { - return PsiReference.EMPTY_ARRAY; - } - - PsiReference[] refs = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, path) - .stream() - .map((file) -> new XLangVfsFileReference(refElement, textRange, file)) - .toArray(PsiReference[]::new); - - if (refs.length > 0) { - return refs; - } - - String msg = NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", path); - return new PsiReference[] { - new XLangNotFoundReference(refElement, textRange, msg) - }; - } - - private List getReferencesFromDictYaml( - XmlElement refElement, String dictPath, Object dictOptionValue, TextRange textRange - ) { - List refs = new ArrayList<>(); - List files = XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, "/dict/" + dictPath + ".dict.yaml"); - - for (PsiFile file : files) { - PsiElement target = XmlPsiHelper.findFirstElement(file, (element) -> { - if (element instanceof LeafPsiElement value // - && dictOptionValue.equals(value.getText()) // - ) { - PsiElement parent = PsiTreeUtil.getParentOfType(element, YAMLKeyValue.class); - PsiElement key = parent != null ? parent.getFirstChild() : null; - - return key != null && "value".equals(key.getText()); - } - return false; - }); - - refs.add(new XLangElementReference(refElement, textRange, target)); - } - - return refs; - } - - private Map extractValuesFromCsv(String csv) { - Map rangePathMap = new HashMap<>(); - - TextScanner sc = TextScanner.fromString(null, csv); - - sc.skipBlank(); - while (!sc.isEnd()) { - int offset = sc.pos; - MutableString buf = sc.useBuf(); - sc.nextUntil(s -> s.cur == ',' || StringHelper.isSpace(sc.cur), sc::appendToBuf); - - String value = buf.toString(); - rangePathMap.put(new TextRange(offset, sc.pos), value); - - sc.next(); - sc.skipBlank(); - } - - return rangePathMap; - } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java deleted file mode 100644 index 564b86b5e..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangVfsFileReference.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.nop.idea.plugin.reference; - -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiReferenceBase; -import com.intellij.psi.xml.XmlElement; -import com.intellij.psi.xml.XmlTag; -import com.intellij.util.ArrayUtil; -import io.nop.idea.plugin.lang.reference.XLangReference; -import io.nop.idea.plugin.utils.XmlPsiHelper; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * 对 Nop vfs 资源文件的引用 - * - * @author flytreeleft - * @date 2025-06-22 - */ -public class XLangVfsFileReference extends PsiReferenceBase implements XLangReference { - private final PsiFile file; - - public XLangVfsFileReference( - @NotNull XmlElement refElement, TextRange textRange, // - @NotNull PsiFile file - ) { - super(refElement, textRange, - // 不采用延迟解析模式,以确保当解析到有其他相关引用时,其能够被 PsiMultiReference 作为最优引用 - false); - this.file = file; - } - - /** 得到具体的引用对象(文件、文件行、文件内某个元素等) */ - @Override - public @Nullable PsiElement resolve() { - PsiElement result = XmlPsiHelper.findFirstElement(file, (element) -> element instanceof XmlTag); - - return result == null ? file : result; - } - - /** 得到补全建议元素列表,可以为字符串或 {@link PsiElement} */ - @Override - public @NotNull Object @NotNull [] getVariants() { - return ArrayUtil.EMPTY_OBJECT_ARRAY; - } - - @Override - public boolean isReferenceTo(@NotNull PsiElement target) { - // XmlAttributeReference#isReferenceTo - PsiManager manager = getElement().getManager(); - return manager.areElementsEquivalent(target, this.file); - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java deleted file mode 100644 index 3c294b52a..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangXDefReference.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.nop.idea.plugin.reference; - -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiReferenceBase; -import com.intellij.psi.xml.XmlElement; -import io.nop.idea.plugin.lang.reference.XLangReference; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * 对 XLang 节点或属性的 XDef 定义元素的引用 - * - * @author flytreeleft - * @date 2025-06-24 - */ -public class XLangXDefReference extends PsiReferenceBase implements XLangReference { - private final XmlElement target; - - public XLangXDefReference(@NotNull XmlElement element, TextRange rangeInElement, XmlElement target) { - super(element, rangeInElement); - this.target = target; - } - - @Override - public @Nullable PsiElement resolve() { - return target; - } - - @Override - public boolean isReferenceTo(@NotNull PsiElement target) { - // XmlAttributeReference#isReferenceTo - PsiManager manager = getElement().getManager(); - return manager.areElementsEquivalent(target, this.target); - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index f70b94431..3bad3998a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -35,6 +35,7 @@ import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTokenType; +import io.nop.api.core.util.ISourceLocationGetter; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.core.resource.ResourceHelper; @@ -43,7 +44,8 @@ import org.jetbrains.annotations.NotNull; public class XmlPsiHelper { - public static String getNopVfsPath(SourceLocation loc) { + public static String getNopVfsPath(ISourceLocationGetter locGetter) { + SourceLocation loc = locGetter != null ? locGetter.getLocation() : null; String path = loc != null ? loc.getPath() : null; // Note: SourceLocation#getPath() 得到的 jar 中的 vfs 路径会添加 classpath:_vfs 前缀 diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index f490167fa..1802a59cd 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -142,32 +142,56 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { } public void testXplReferences() { + // xlib 中的 source 标签内的引用 assertReference(""" - - - ort from="/test/reference/a.xlib"/> - - - """, ""); + + + urce> + + + + """, "/nop/schema/xlib.xdef?source"); assertReference(""" - - - ipt> - - - """, ""); + + + + bc xpl:if="true"/> + + + + + """, "/nop/schema/xpl.xdef?xdef:unknown-tag"); + +// // TODO xpl 节点内引用内置的 xpl 标签函数 +// assertReference(""" +// +// +// ort from="/test/reference/a.xlib"/> +// +// +// """, ""); +// assertReference(""" +// +// +// ipt> +// +// +// """, ""); // 通过 xpl:lib 导入 xlib assertReference(""" ByMdxQuery xpl:lib="/test/reference/a.xlib"/> @@ -177,7 +201,6 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(""" hod="post" xpl:lib="/test/reference/a.xlib"/> @@ -189,7 +212,6 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(""" @@ -200,7 +222,6 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(""" @@ -211,7 +232,6 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(""" @@ -222,7 +242,6 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { assertReference(""" diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java index 40f991b42..ebebc4235 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java @@ -130,6 +130,33 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase { """); + assertCompletionInXLib(""" + + + + + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const handler = new XJsonDomainHandler(); + handler.getN(); + + + + + """, """ + + + + + import io.nop.xlang.xdef.domain.XJsonDomainHandler; + const handler = new XJsonDomainHandler(); + handler.getName(); + + + + + """); } /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */ diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index 0556c31b9..ed8cfd3ab 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -2,8 +2,14 @@ x:schema="/nop/schema/xlib.xdef" > - + + + import io.nop.commons.util.StringHelper; + + StringHelper.escapeXml('abc'); + + -- Gitee From 726a02d7f4141556a038baffbe51979eab75d4b7 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 17 Jul 2025 18:12:08 +0800 Subject: [PATCH 63/82] =?UTF-8?q?nop-idea-pugin:=20=E6=89=A9=E5=A4=A7=20cl?= =?UTF-8?q?ass=20=E6=90=9C=E7=B4=A2=E8=8C=83=E5=9B=B4=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E5=9C=A8=20dsl=20=E4=B8=AD=E5=BC=95=E5=85=A5=E7=9A=84?= =?UTF-8?q?=20class=20=E8=83=BD=E5=A4=9F=E8=A2=AB=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nop/idea/plugin/utils/PsiClassHelper.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java index eaf498c48..144fd663e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -14,8 +14,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.psi.CommonClassNames; import com.intellij.psi.JavaPsiFacade; @@ -59,7 +57,17 @@ import org.jetbrains.annotations.NotNull; * @date 2025-06-26 */ public class PsiClassHelper { - private static final JavaClassReferenceProvider javaClassRefProvider = new JavaClassReferenceProvider(); + private static final JavaClassReferenceProvider javaClassRefProvider = new JavaClassReferenceProvider() { + @Override + public @NotNull GlobalSearchScope getScope(@NotNull Project project) { +// Module module = ModuleUtilCore.findModuleForPsiElement(element); +// scope = module == null +// ? GlobalSearchScope.allScope(project) +// : module.getModuleWithDependenciesAndLibrariesScope(true); + // Note: DSL 可能定义在独立的项目中,因此,其可能引入非其所在模块依赖包中的 class + return GlobalSearchScope.allScope(project); + } + }; private static final Map primitiveTypeWrapper = Map.of(PsiTypes.byteType(), "java.lang.Byte", @@ -88,15 +96,7 @@ public class PsiClassHelper { public static @NotNull GlobalSearchScope getSearchScope(@NotNull PsiElement element) { Project project = element.getProject(); - GlobalSearchScope scope = javaClassRefProvider.getScope(project); - if (scope == null) { - Module module = ModuleUtilCore.findModuleForPsiElement(element); - - scope = module == null - ? GlobalSearchScope.allScope(project) - : module.getModuleWithDependenciesAndLibrariesScope(true); - } - return scope; + return javaClassRefProvider.getScope(project); } public static PsiReference @NotNull [] createJavaClassReferences( @@ -125,7 +125,6 @@ public class PsiClassHelper { return null; } - Project project = context.getProject(); // 处理通配符泛型 if (type instanceof PsiWildcardType t) { PsiType bound = t.getBound(); -- Gitee From b65f5c5fb677742bffd58d52f6cad4b6c6ded265 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 17 Jul 2025 19:46:04 +0800 Subject: [PATCH 64/82] =?UTF-8?q?nop-idea-pugin:=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=8C=85=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{lang/highlight => annotator}/XLangAnnotator.java | 6 +++--- .../XLangHighlightRangeExtension.java | 2 +- .../idea/plugin/lang/script/XLangScriptFileType.java | 2 +- .../src/main/resources/META-INF/plugin.xml | 4 ++-- .../io/nop/idea/plugin/BaseXLangPluginTestCase.java | 11 +---------- .../plugin/doc/TestXLangDocumentationProvider.java | 2 +- 6 files changed, 9 insertions(+), 18 deletions(-) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/{lang/highlight => annotator}/XLangAnnotator.java (99%) rename nop-idea-plugin/src/main/java/io/nop/idea/plugin/{lang/highlight => annotator}/XLangHighlightRangeExtension.java (95%) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java similarity index 99% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangAnnotator.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java index 66a851130..1a98846d7 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java @@ -1,11 +1,11 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. * Author: canonical_entropy@163.com * Blog: https://www.zhihu.com/people/canonical-entropy * Gitee: https://gitee.com/canonical-entropy/nop-entropy * Github: https://github.com/entropy-cloud/nop-entropy */ -package io.nop.idea.plugin.lang.highlight; +package io.nop.idea.plugin.annotator; import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension; import com.intellij.codeInspection.ProblemHighlightType; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangHighlightRangeExtension.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangHighlightRangeExtension.java similarity index 95% rename from nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangHighlightRangeExtension.java rename to nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangHighlightRangeExtension.java index 0970bda85..b29ea3e27 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/highlight/XLangHighlightRangeExtension.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangHighlightRangeExtension.java @@ -6,7 +6,7 @@ * Github: https://github.com/entropy-cloud/nop-entropy */ -package io.nop.idea.plugin.lang.highlight; +package io.nop.idea.plugin.annotator; import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension; import com.intellij.psi.PsiFile; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptFileType.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptFileType.java index a303afb5d..8753a69a8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptFileType.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptFileType.java @@ -39,7 +39,7 @@ public class XLangScriptFileType extends LanguageFileType { @NotNull @Override public String getDescription() { - return "XLang script file"; + return "XLang Script (Embedded in XLang DSL)"; } @NotNull diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index e09d07b19..1b0b9934a 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -87,8 +87,8 @@ implementationClass="io.nop.idea.plugin.lang.XLangParserDefinition"/> - + implementationClass="io.nop.idea.plugin.annotator.XLangAnnotator"/> + diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 75161c5a5..2ff8056d1 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -23,7 +23,6 @@ import com.intellij.codeInsight.lookup.Lookup; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupManager; import com.intellij.codeInsight.lookup.impl.LookupImpl; -import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.FileTypeManager; @@ -189,7 +188,7 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur return ref; } - protected String getDoc() { + protected String getDocAtCaret() { // Note: 通过 ApplicationManager.getApplication().runReadAction(() -> {}) // 消除异常 "Read access is allowed from inside read-action" PsiElement originalElement = getElementAtCaret(); @@ -202,14 +201,6 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur return docProvider.generateDoc(element, originalElement); } - protected PsiElement[] getGotoTargets() { - assertCaretExists(); - - return GotoDeclarationAction.findAllTargetElements(getProject(), - myFixture.getEditor(), - myFixture.getCaretOffset()); - } - private void assertCaretExists() { assertTrue("No '' found in current text", myFixture.getCaretOffset() > 0); } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index 163f45674..6f7fa0b83 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -139,7 +139,7 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { private void doTest(String text, Consumer checker) { configureByXLangText(text); - String genDoc = getDoc(); + String genDoc = getDocAtCaret(); checker.accept(genDoc); } -- Gitee From 28aaeac04c7958a44b46290a2eec90acf04d00dc Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 17 Jul 2025 19:50:19 +0800 Subject: [PATCH 65/82] =?UTF-8?q?nop-idea-pugin:=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=97=A0=E7=94=A8=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc/AntDomDocumentationProvider.java | 247 ------------------ .../plugin/refactoring/AntRenameHandler.java | 80 ------ .../idea/plugin/utils/TextAttributeKeys.java | 24 -- .../io/nop/idea/plugin/xml/XmlTagAdapter.java | 85 ------ 4 files changed, 436 deletions(-) delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/AntDomDocumentationProvider.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/refactoring/AntRenameHandler.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/TextAttributeKeys.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/xml/XmlTagAdapter.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/AntDomDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/AntDomDocumentationProvider.java deleted file mode 100644 index d21d2616f..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/AntDomDocumentationProvider.java +++ /dev/null @@ -1,247 +0,0 @@ -//// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -//package io.nop.idea.plugin.doc; -// -//import com.intellij.lang.ant.AntFilesProvider; -//import com.intellij.lang.ant.AntSupport; -//import com.intellij.lang.ant.config.impl.AntInstallation; -//import com.intellij.lang.ant.dom.AntDomElement; -//import com.intellij.lang.ant.dom.AntDomProject; -//import com.intellij.lang.ant.dom.AntDomTarget; -//import com.intellij.lang.documentation.DocumentationProvider; -//import com.intellij.openapi.diagnostic.Logger; -//import com.intellij.openapi.util.NlsSafe; -//import com.intellij.openapi.util.io.FileUtil; -//import com.intellij.openapi.vfs.JarFileSystem; -//import com.intellij.openapi.vfs.LocalFileSystem; -//import com.intellij.openapi.vfs.VfsUtilCore; -//import com.intellij.openapi.vfs.VirtualFile; -//import com.intellij.openapi.vfs.VirtualFileManager; -//import com.intellij.pom.PomTarget; -//import com.intellij.pom.PomTargetPsiElement; -//import com.intellij.psi.PsiElement; -//import com.intellij.psi.PsiFile; -//import com.intellij.psi.util.PsiTreeUtil; -//import com.intellij.psi.xml.XmlElement; -//import com.intellij.psi.xml.XmlTag; -//import com.intellij.util.xml.DomElement; -//import com.intellij.util.xml.DomTarget; -//import com.intellij.util.xml.reflect.DomChildrenDescription; -//import org.jetbrains.annotations.Nls; -//import org.jetbrains.annotations.NonNls; -//import org.jetbrains.annotations.Nullable; -// -//import java.io.File; -//import java.io.IOException; -//import java.lang.reflect.Type; -//import java.util.Collections; -//import java.util.HashSet; -//import java.util.List; -// -//public class AntDomDocumentationProvider implements DocumentationProvider { -// -// private static final Logger LOG = Logger.getInstance(AntDomDocumentationProvider.class); -// -// @Override -// public @Nls String generateDoc(PsiElement element, PsiElement originalElement) { -// final String mainDoc = getMainDocumentation(originalElement); -// final String additionalDoc = getAdditionalDocumentation(originalElement); -// if (mainDoc == null && additionalDoc == null) { -// return null; -// } -// @NlsSafe final StringBuilder builder = new StringBuilder(); -// if (additionalDoc != null) { -// builder.append(additionalDoc); -// } -// if (mainDoc != null) { -// builder.append(mainDoc); -// } -// return builder.toString(); -// } -// -// @Nullable -// private static String getMainDocumentation(PsiElement elem) { -// final VirtualFile helpFile = getHelpFile(elem); -// if (helpFile != null) { -// try { -// return VfsUtilCore.loadText(helpFile); -// } -// catch (IOException ignored) { -// } -// } -// return null; -// } -// -// @Nullable -// private static String getAdditionalDocumentation(PsiElement elem) { -// final XmlTag xmlTag = PsiTreeUtil.getParentOfType(elem, XmlTag.class); -// if (xmlTag == null) { -// return null; -// } -// final AntDomElement antElement = AntSupport.getAntDomElement(xmlTag); -// if (antElement instanceof AntFilesProvider) { -// final List list = ((AntFilesProvider)antElement).getFiles(new HashSet<>()); -// if (list.size() > 0) { -// final @NonNls StringBuilder builder = new StringBuilder(); -// final XmlTag tag = antElement.getXmlTag(); -// if (tag != null) { -// builder.append(""); -// builder.append(tag.getName()); -// builder.append(":"); -// } -// for (File file : list) { -// if (builder.length() > 0) { -// builder.append("
"); -// } -// builder.append(file.getPath()); -// } -// return builder.toString(); -// } -// } -// return null; -// } -// -// @Nullable -// private static VirtualFile getHelpFile(final PsiElement element) { -// final XmlTag xmlTag = PsiTreeUtil.getParentOfType(element, XmlTag.class); -// if (xmlTag == null) { -// return null; -// } -// final AntDomElement antElement = AntSupport.getAntDomElement(xmlTag); -// if (antElement == null) { -// return null; -// } -// final AntDomProject antProject = antElement.getAntProject(); -// if (antProject == null) { -// return null; -// } -// final AntInstallation installation = antProject.getAntInstallation(); -// if (installation == null) { -// return null; // not configured properly and bundled installation missing -// } -// final String antHomeDir = AntInstallation.HOME_DIR.get(installation.getProperties()); -// -// if (antHomeDir == null) { -// return null; -// } -// -// @NonNls String path = antHomeDir + "/docs/manual"; -// String url; -// if (new File(path).exists()) { -// url = VirtualFileManager.constructUrl(LocalFileSystem.PROTOCOL, FileUtil.toSystemIndependentName(path)); -// } -// else { -// path = antHomeDir + "/docs.zip"; -// if (new File(path).exists()) { -// url = VirtualFileManager.constructUrl(JarFileSystem.PROTOCOL, FileUtil.toSystemIndependentName(path) + JarFileSystem.JAR_SEPARATOR + "docs/manual"); -// } -// else { -// return null; -// } -// } -// -// final VirtualFile documentationRoot = VirtualFileManager.getInstance().findFileByUrl(url); -// if (documentationRoot == null) { -// return null; -// } -// -// return getHelpFile(antElement, documentationRoot); -// } -// -// public static final String[] DOC_FOLDER_NAMES = new String[] { -// "Tasks", "Types", "CoreTasks", "OptionalTasks", "CoreTypes", "OptionalTypes" -// }; -// -// @Nullable -// private static VirtualFile getHelpFile(AntDomElement antElement, final VirtualFile documentationRoot) { -// final XmlTag xmlTag = antElement.getXmlTag(); -// if (xmlTag == null) { -// return null; -// } -// @NonNls final String helpFileShortName = "/" + xmlTag.getName() + ".html"; -// -// for (String folderName : DOC_FOLDER_NAMES) { -// final VirtualFile candidateHelpFile = documentationRoot.findFileByRelativePath(folderName + helpFileShortName); -// if (candidateHelpFile != null) { -// return candidateHelpFile; -// } -// } -// -// if(antElement instanceof AntDomTarget|| antElement instanceof AntDomProject) { -// final VirtualFile candidateHelpFile = documentationRoot.findFileByRelativePath("using.html"); -// if (candidateHelpFile != null) { -// return candidateHelpFile; -// } -// } -// -// return null; -// } -// -// @Override -// @Nullable -// public @Nls String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { -// if (element instanceof PomTargetPsiElement) { -// final PomTarget pomTarget = ((PomTargetPsiElement)element).getTarget(); -// if (pomTarget instanceof DomTarget) { -// final DomElement domElement = ((DomTarget)pomTarget).getDomElement(); -// if (domElement instanceof AntDomTarget) { -// final AntDomTarget antTarget = (AntDomTarget)domElement; -// final String description = antTarget.getDescription().getRawText(); -// if (description != null && description.length() > 0) { -// final String targetName = antTarget.getName().getRawText(); -// final StringBuilder builder = new StringBuilder(); -// builder.append("Target"); -// if (targetName != null) { -// builder.append(" \"").append(targetName).append("\""); -// } -// final XmlElement xmlElement = antTarget.getXmlElement(); -// if (xmlElement != null) { -// final PsiFile containingFile = xmlElement.getContainingFile(); -// if (containingFile != null) { -// final String fileName = containingFile.getName(); -// builder.append(" [").append(fileName).append("]"); -// } -// } -// @NlsSafe final String result = builder.append(" ").append(description).toString(); -// return result; -// } -// } -// } -// else if (pomTarget instanceof DomChildrenDescription) { -// final DomChildrenDescription description = (DomChildrenDescription)pomTarget; -// Type type = null; -// try { -// type = description.getType(); -// } -// catch (UnsupportedOperationException e) { -// LOG.info(e); -// } -// if (type instanceof Class && AntDomElement.class.isAssignableFrom(((Class)type))) { -// final String elemName = description.getName(); -// if (elemName != null) { -// final AntDomElement.Role role = description.getUserData(AntDomElement.ROLE); -// final StringBuilder builder = new StringBuilder(); -// if (role == AntDomElement.Role.TASK) { -// builder.append("Task "); -// } -// else if (role == AntDomElement.Role.DATA_TYPE) { -// builder.append("Data structure "); -// } -// builder.append(elemName); -// @NlsSafe final String result = builder.toString(); -// return result; -// } -// } -// } -// } -// return null; -// } -// -// @Override -// public List getUrlFor(PsiElement element, PsiElement originalElement) { -// final VirtualFile helpFile = getHelpFile(originalElement); -// if (helpFile == null || !(helpFile.getFileSystem() instanceof LocalFileSystem)) { -// return null; -// } -// return Collections.singletonList(helpFile.getUrl()); -// } -//} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/refactoring/AntRenameHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/refactoring/AntRenameHandler.java deleted file mode 100644 index 31d9f61f3..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/refactoring/AntRenameHandler.java +++ /dev/null @@ -1,80 +0,0 @@ -///* -// * Copyright 2000-2010 JetBrains s.r.o. -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ -//package io.nop.idea.plugin.refactoring; -// -//import com.intellij.codeInsight.TargetElementUtil; -//import com.intellij.lang.ant.dom.AntDomFileDescription; -//import com.intellij.openapi.actionSystem.CommonDataKeys; -//import com.intellij.openapi.actionSystem.DataContext; -//import com.intellij.openapi.editor.Editor; -//import com.intellij.openapi.project.IndexNotReadyException; -//import com.intellij.openapi.project.Project; -//import com.intellij.psi.PsiElement; -//import com.intellij.psi.PsiFile; -//import com.intellij.psi.PsiReference; -//import com.intellij.psi.xml.XmlFile; -//import com.intellij.refactoring.rename.PsiElementRenameHandler; -//import org.jetbrains.annotations.NotNull; -//import org.jetbrains.annotations.Nullable; -// -//import java.util.Collection; -// -///** -// * @author Eugene Zhuravlev -// */ -//public final class AntRenameHandler extends PsiElementRenameHandler { -// -// @Override -// public boolean isAvailableOnDataContext(@NotNull final DataContext dataContext) { -// final PsiElement[] elements = getElements(dataContext); -// return elements != null && elements.length > 1; -// } -// -// @Override -// public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file, @NotNull final DataContext dataContext) { -// final PsiElement[] elements = getElements(dataContext); -// if (elements != null && elements.length > 0) { -// invoke(project, new PsiElement[]{elements[0]}, dataContext); -// } -// } -// -// private static PsiElement @Nullable [] getElements(DataContext dataContext) { -// final PsiFile psiFile = CommonDataKeys.PSI_FILE.getData(dataContext); -// if (!(psiFile instanceof XmlFile && AntDomFileDescription.isAntFile((XmlFile)psiFile))) { -// return null; -// } -// final Editor editor = CommonDataKeys.EDITOR.getData(dataContext); -// if (editor == null) { -// return null; -// } -// return getPsiElementsIn(editor); -// } -// -// private static PsiElement @Nullable [] getPsiElementsIn(final Editor editor) { -// try { -// final PsiReference reference = TargetElementUtil.findReference(editor, editor.getCaretModel().getOffset()); -// if (reference == null) { -// return null; -// } -// final Collection candidates = TargetElementUtil.getInstance().getTargetCandidates(reference); -// return candidates.toArray(PsiElement.EMPTY_ARRAY); -// } -// catch (IndexNotReadyException e) { -// return null; -// } -// } -// -//} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/TextAttributeKeys.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/TextAttributeKeys.java deleted file mode 100644 index 2d582726f..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/TextAttributeKeys.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ -package io.nop.idea.plugin.utils; - -import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; -import com.intellij.openapi.editor.colors.TextAttributesKey; - -import static com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey; - -public class TextAttributeKeys { - public static final TextAttributesKey FUNC = - createTextAttributesKey("XLANG_FUNC", DefaultLanguageHighlighterColors.FUNCTION_DECLARATION); - public static final TextAttributesKey ATTR = - createTextAttributesKey("XLANG_ATTR", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey VALUE = - createTextAttributesKey("XLANG_VALUE", DefaultLanguageHighlighterColors.LABEL); - - public static final TextAttributesKey TAG = createTextAttributesKey("XLANG_TAG", DefaultLanguageHighlighterColors.MARKUP_TAG); -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/xml/XmlTagAdapter.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/xml/XmlTagAdapter.java deleted file mode 100644 index d348a236f..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/xml/XmlTagAdapter.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ -package io.nop.idea.plugin.xml; - -import com.intellij.psi.PsiElement; -import com.intellij.psi.xml.XmlTag; -import com.intellij.psi.xml.XmlTagValue; -import io.nop.core.lang.xml.adapter.IXNodeViewAdapter; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class XmlTagAdapter implements IXNodeViewAdapter { - public static XmlTagAdapter INSTANCE = new XmlTagAdapter(); - - @Override - public String tagName(XmlTag xmlTag) { - return xmlTag.getName(); - } - - @Override - public Object attr(XmlTag xmlTag, String name) { - return xmlTag.getAttributeValue(name); - } - - @Override - public XmlTag getParent(XmlTag xmlTag) { - PsiElement elm = xmlTag.getParent(); - if (elm instanceof XmlTag) - return (XmlTag) elm; - return null; - } - - @Override - public List getChildren(XmlTag xmlTag) { - PsiElement[] children = xmlTag.getChildren(); - if (children == null || children.length <= 0) - return Collections.emptyList(); - - List ret = new ArrayList<>(children.length); - for (PsiElement elm : children) { - if (elm instanceof XmlTag) { - ret.add((XmlTag) elm); - } - } - return ret; - } - - @Override - public Object value(XmlTag xmlTag) { - return text(xmlTag); - } - - @Override - public String xml(XmlTag xmlTag) { - return null; - } - - @Override - public String innerXml(XmlTag xmlTag) { - return null; - } - - @Override - public String html(XmlTag xmlTag) { - return null; - } - - @Override - public String innerHtml(XmlTag xmlTag) { - return null; - } - - @Override - public String text(XmlTag xmlTag) { - XmlTagValue value = xmlTag.getValue(); - return value == null ? null : value.getText(); - } -} -- Gitee From a1208494a3241ca248ec020b14a85450fc15a862 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 18 Jul 2025 21:59:30 +0800 Subject: [PATCH 66/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B=20XL?= =?UTF-8?q?ang=20=E8=8A=82=E7=82=B9=E5=92=8C=E5=B1=9E=E6=80=A7=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E7=9A=84=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../idea/plugin/annotator/XLangAnnotator.java | 2 +- .../doc/XLangDocumentationProvider.java | 165 ++---- .../idea/plugin/lang/XLangDocumentation.java | 102 ++++ .../io/nop/idea/plugin/lang/psi/XLangTag.java | 108 +++- .../idea/plugin/lang/psi/XLangTextToken.java | 2 +- .../lang/reference/XLangDefAttrReference.java | 9 +- .../XLangParentTagAttrReference.java | 4 +- .../lang/reference/XLangTagReference.java | 5 +- .../nop/idea/plugin/utils/XmlPsiHelper.java | 7 +- .../src/main/resources/META-INF/plugin.xml | 4 +- .../idea/plugin/BaseXLangPluginTestCase.java | 4 + .../doc/TestXLangDocumentationProvider.java | 512 ++++++++++++++---- 12 files changed, 666 insertions(+), 258 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java index 1a98846d7..3eb2c5fdc 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java @@ -159,7 +159,7 @@ public class XLangAnnotator implements Annotator { } private void checkTagValue(@NotNull AnnotationHolder holder, @NotNull XLangTag tag) { - XDefTypeDecl xdefValue = tag.getDefNodeXdefValue(); + XDefTypeDecl xdefValue = tag.getSchemaDefNodeXdefValue(); TextRange textRange = tag.getValue().getTextRange(); String bodyText = tag.hasChildTag() ? null : tag.getBodyText(); boolean blankBodyText = StringHelper.isBlank(bodyText); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java index 2d0526633..44b2457bd 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java @@ -11,61 +11,53 @@ import java.util.Objects; import com.intellij.lang.documentation.AbstractDocumentationProvider; import com.intellij.psi.PsiElement; +import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlTag; -import com.intellij.psi.xml.XmlTokenType; import io.nop.api.core.beans.DictBean; import io.nop.api.core.beans.DictOptionBean; -import io.nop.commons.util.StringHelper; import io.nop.core.dict.DictProvider; -import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.idea.plugin.lang.XLangDocumentation; +import io.nop.idea.plugin.lang.psi.XLangAttribute; +import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.utils.MarkdownHelper; import io.nop.idea.plugin.utils.XDefPsiHelper; -import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.utils.XmlTagInfo; -import io.nop.xlang.xdef.IXDefComment; -import io.nop.xlang.xdef.IXDefNode; -import io.nop.xlang.xdef.IXDefSubComment; import io.nop.xlang.xdef.XDefTypeDecl; import jakarta.annotation.Nullable; -import org.jetbrains.annotations.NotNull; -public class XLangDocumentationProvider extends AbstractDocumentationProvider { +import static com.intellij.psi.xml.XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN; +import static com.intellij.psi.xml.XmlTokenType.XML_NAME; - @Override - public @Nullable String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { - return ProjectEnv.withProject(element.getProject(), () -> { - if (XmlPsiHelper.isElementType(originalElement, XmlTokenType.XML_NAME)) { - return generateDocForXmlName(originalElement); - } // - else if (XmlPsiHelper.isElementType(originalElement, XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)) { - return generateDocForXmlAttributeValue(originalElement); - } - return null; - }); - } +public class XLangDocumentationProvider extends AbstractDocumentationProvider { /** - * Provides documentation when a Simple Language element is hovered with the mouse. + * 文档生成函数 + *

+ * 默认鼠标移动时的文档也由该函数生成 {@link #generateHoverDoc} + * + * @param srcElement + * 当前鼠标下的元素,对于 xml 标签和属性,其为 + * {@link com.intellij.psi.xml.XmlTokenType#XML_NAME XML_NAME} 类型,对于属性值,其为 + * {@link com.intellij.psi.xml.XmlTokenType#XML_ATTRIBUTE_VALUE_TOKEN XML_ATTRIBUTE_VALUE_TOKEN} + * 类型 + * @param resolvedElement + * 根据 srcElement 所识别出的被引用元素 */ @Override - public @Nullable String generateHoverDoc(@NotNull PsiElement element, @Nullable PsiElement originalElement) { - return generateDoc(element, originalElement); - } + public @Nullable String generateDoc(PsiElement resolvedElement, @Nullable PsiElement srcElement) { + if (srcElement == null) { + return null; + } + + IElementType elementType = srcElement.getNode().getElementType(); + if (elementType == XML_NAME) { + return generateDocForXmlName(srcElement); + } // + else if (elementType == XML_ATTRIBUTE_VALUE_TOKEN) { + return generateDocForXmlAttributeValue(srcElement); + } - /** - * ctrl + hover时显示的文档信息 - *

- * Provides the information in which file the Simple language key/value is defined. - */ - @Override - public @Nullable String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { -// if (element instanceof SimpleProperty) { -// final String key = ((SimpleProperty) element).getKey(); -// final String file = SymbolPresentationUtil.getFilePathPresentation(element.getContainingFile()); -// return "\"" + key + "\" in " + file; -// } return null; } @@ -73,36 +65,18 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { private String generateDocForXmlName(PsiElement element) { PsiElement parent = element.getParent(); - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(parent); - if (tagInfo == null || tagInfo.getDefNode() == null) { - return null; - } - - if (parent instanceof XmlTag tag) { - DocInfo doc = new DocInfo(tagInfo.getDefNode()); - doc.setMainTitle(tag.getName()); - - IXDefComment comment = tagInfo.getDefNodeComment(); - if (comment != null) { - doc.setSubTitle(comment.getMainDisplayName()); - doc.setDesc(comment.getMainDescription()); - } - return doc.toString(); - } else if (parent instanceof XmlAttribute attr) { + XLangDocumentation doc = null; + if (parent instanceof XLangTag tag) { + doc = tag.getTagDocumentation(); + } // + else if (parent instanceof XLangAttribute attr) { String attrName = attr.getName(); + XLangTag tag = attr.getParentTag(); - DocInfo doc = new DocInfo(tagInfo.getDefAttrType(attrName)); - doc.setMainTitle(attrName); - - IXDefSubComment comment = tagInfo.getDefAttrComment(attrName); - if (comment != null) { - doc.setSubTitle(comment.getDisplayName()); - doc.setDesc(comment.getDescription()); - } - return doc.toString(); + doc = tag != null ? tag.getAttrDocumentation(attrName) : null; } - return null; + return doc != null ? doc.toString() : null; } /** 为 xml 属性值生成文档 */ @@ -129,7 +103,7 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { String value = option.getStringValue(); String label = option.getLabel(); - DocInfo doc = new DocInfo(); + XLangDocumentation doc = new XLangDocumentation(dictBean); doc.setMainTitle(value); if (label != null && !Objects.equals(label, value)) { @@ -147,65 +121,4 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { return text; } - - static class DocInfo { - String mainTitle; - String subTitle; - String stdDomain; - String desc; - - DocInfo() { - } - - DocInfo(IXDefNode defNode) { - this(defNode.getXdefValue()); - } - - DocInfo(XDefTypeDecl type) { - if (type != null) { - this.stdDomain = type.getStdDomain(); - if (type.getOptions() != null) { - this.stdDomain += ':' + type.getOptions(); - } - } - } - - public void setMainTitle(String mainTitle) { - this.mainTitle = mainTitle; - } - - public void setSubTitle(String subTitle) { - this.subTitle = subTitle; - } - - public void setDesc(String desc) { - this.desc = desc; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - sb.append("

"); - sb.append(StringHelper.escapeXml(this.mainTitle)); - if (StringHelper.isNotBlank(this.subTitle)) { - sb.append(" - ").append(StringHelper.escapeXml(this.subTitle)); - } - sb.append("

"); - - if (this.stdDomain != null) { - sb.append("

"); - sb.append("stdDomain: "); - sb.append("").append(StringHelper.escapeXml(this.stdDomain)).append(""); - sb.append("

"); - } - - if (!StringHelper.isBlank(this.desc)) { - sb.append("

"); - sb.append(markdown(this.desc)); - } - - return !sb.isEmpty() ? sb.toString() : null; - } - } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java new file mode 100644 index 000000000..59aa9518e --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang; + +import io.nop.api.core.util.ISourceLocationGetter; +import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.doc.XLangDocumentationProvider; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.xlang.xdef.IXDefAttribute; +import io.nop.xlang.xdef.IXDefNode; +import io.nop.xlang.xdef.XDefTypeDecl; + +/** + * XLang 说明文档 + * + * @author flytreeleft + * @date 2025-07-18 + */ +public class XLangDocumentation { + String mainTitle; + String subTitle; + String stdDomain; + boolean required; + String desc; + String path; + + public XLangDocumentation(ISourceLocationGetter locGetter) { + this(locGetter, null); + } + + public XLangDocumentation(IXDefNode defNode) { + this(defNode, defNode.getXdefValue()); + } + + public XLangDocumentation(IXDefAttribute attrDef) { + this(attrDef, attrDef.getType()); + } + + XLangDocumentation(ISourceLocationGetter locGetter, XDefTypeDecl type) { + this.path = XmlPsiHelper.getNopVfsPath(locGetter); + + if (type != null) { + this.required = type.isMandatory(); + this.stdDomain = type.getStdDomain(); + if (type.getOptions() != null) { + this.stdDomain += ':' + type.getOptions(); + } + } + } + + public void setMainTitle(String mainTitle) { + this.mainTitle = mainTitle; + } + + public void setSubTitle(String subTitle) { + this.subTitle = subTitle; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("

"); + sb.append(StringHelper.escapeXml(this.mainTitle)); + if (StringHelper.isNotBlank(this.subTitle)) { + sb.append(" - ").append(StringHelper.escapeXml(this.subTitle)); + } + sb.append("

"); + + if (this.stdDomain != null) { + sb.append("

"); + sb.append("stdDomain: "); + sb.append(this.required ? "[Required] " : "[Option] "); + sb.append("").append(StringHelper.escapeXml(this.stdDomain)).append(""); + sb.append("

"); + } + + if (this.path != null) { + sb.append("

"); + sb.append("vfs: "); + sb.append("").append(StringHelper.escapeXml(this.path)).append(""); + sb.append("

"); + } + + if (!StringHelper.isBlank(this.desc)) { + sb.append("

"); + sb.append(XLangDocumentationProvider.markdown(this.desc)); + } + + return !sb.isEmpty() ? sb.toString() : null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 3c90d87a7..9ac35571c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -24,12 +24,15 @@ import com.intellij.xml.util.XmlTagUtil; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.core.lang.xml.XNode; +import io.nop.idea.plugin.lang.XLangDocumentation; import io.nop.idea.plugin.lang.reference.XLangTagReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.xlang.xdef.IXDefAttribute; +import io.nop.xlang.xdef.IXDefComment; import io.nop.xlang.xdef.IXDefNode; +import io.nop.xlang.xdef.IXDefSubComment; import io.nop.xlang.xdef.IXDefinition; import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefKeys; @@ -182,7 +185,7 @@ public class XLangTag extends XmlTagImpl { * 获取 {@link #getSchemaDefNode()} 节点的 {@link XDefKeys#VALUE xdef:value}, * 即,其子节点(包括文本节点)对应的{@link XDefTypeDecl 类型} */ - public XDefTypeDecl getDefNodeXdefValue() { + public XDefTypeDecl getSchemaDefNodeXdefValue() { IXDefNode defNode = getSchemaDefNode(); return defNode != null ? defNode.getXdefValue() : null; @@ -257,9 +260,9 @@ public class XLangTag extends XmlTagImpl { return isXdefValueSupportBody(); } - /** 当前标签的 {@link #getDefNodeXdefValue() xdef:value} 类型是否支持内嵌节点 */ + /** 当前标签的 {@link #getSchemaDefNodeXdefValue() xdef:value} 类型是否支持内嵌节点 */ public boolean isXdefValueSupportBody() { - XDefTypeDecl xdefValue = getDefNodeXdefValue(); + XDefTypeDecl xdefValue = getSchemaDefNodeXdefValue(); return xdefValue != null && xdefValue.isSupportBody(StdDomainRegistry.instance()); } @@ -284,6 +287,101 @@ public class XLangTag extends XmlTagImpl { || !def.getXdefCheckNs().contains(ns); } + /** 获取当前标签的说明文档 */ + public XLangDocumentation getTagDocumentation() { + String tagNs = getNamespacePrefix(); + String tagName = getName(); + + IXDefNode defNode; + // xdef:define 没有实体节点,故而,不显示其文档 + if (getXDefKeys().DEFINE.equals(tagName)) { + defNode = null; + } + // x 名字空间的节点,显示其在 xdsl 中的文档 + else if (XDslKeys.DEFAULT.NS.equals(tagNs)) { + defNode = getXDslDefNode(); + } + // 在非 xdef.xdef 中的以 xdef 为名字空间的节点(不含 xdef:unknown-tag),显示其定义文档 + else if (!isInXDefXDef() && XDefKeys.DEFAULT.NS.equals(tagNs) // + && !XDefKeys.DEFAULT.UNKNOWN_TAG.equals(tagName) // + ) { + defNode = getSchemaDefNode(); + } + // *.xdef 中的自定义节点,显示自己的文档 + else if (getSelfDefNode() != null) { + defNode = getSelfDefNode(); + } + // 其余 dsl 均显示节点的定义文档 + else { + defNode = getSchemaDefNode(); + } + + if (defNode == null) { + return null; + } + + XLangDocumentation doc = new XLangDocumentation(defNode); + doc.setMainTitle(tagName); + + IXDefComment comment = defNode.getComment(); + if (comment != null) { + doc.setSubTitle(comment.getMainDisplayName()); + doc.setDesc(comment.getMainDescription()); + } + + return doc; + } + + /** 获取当前标签指定属性的说明文档 */ + public XLangDocumentation getAttrDocumentation(String attrName) { + String attrNs = StringHelper.getNamespace(attrName); + if ("xmlns".equals(attrNs)) { + return null; + } + + String mainTitle = attrName; + IXDefNode defNode; + if (isInXDefXDef() && XDefKeys.DEFAULT.NS.equals(attrNs)) { + defNode = getSelfDefNode(); + } // + else if (XDslKeys.DEFAULT.NS.equals(attrNs) || getXDslKeys().NS.equals(attrNs)) { + attrName = changeNamespace(attrName, getXDslKeys().NS, XDslKeys.DEFAULT.NS); + defNode = getXDslDefNode(); + } // + else { + attrName = changeNamespace(attrName, getXDefKeys().NS, XDefKeys.DEFAULT.NS); + + defNode = getSelfDefNode(); + // 对于 *.xdef,优先取其自身定义节点上的属性文档 + if (defNode == null || defNode.getAttribute(attrName) == null) { + defNode = getSchemaDefNode(); + } + } + + IXDefAttribute attrDef = getXDefNodeAttr(defNode, attrName); + if (attrDef == null) { + return null; + } + + XLangDocumentation doc = new XLangDocumentation(attrDef); + doc.setMainTitle(mainTitle); + + IXDefComment nodeComment = defNode.getComment(); + if (nodeComment != null) { + IXDefSubComment attrComment = nodeComment.getSubComments().get(attrName); + if (attrComment == null && attrDef.isUnknownAttr()) { + attrComment = nodeComment.getSubComments().get(XDefKeys.DEFAULT.UNKNOWN_ATTR); + } + + if (attrComment != null) { + doc.setSubTitle(attrComment.getDisplayName()); + doc.setDesc(attrComment.getDescription()); + } + } + + return doc; + } + /** 获取 {@link IXDefNode} 上指定属性的 xdef 定义 */ private IXDefAttribute getXDefNodeAttr(IXDefNode xdefNode, String attrName) { String xmlnsPrefix = "xmlns:"; @@ -401,14 +499,14 @@ public class XLangTag extends XmlTagImpl { IXDefNode selfDefNode = parentSelfDefNode != null ? parentSelfDefNode.getChild(tagName) : null; IXDefNode schemaDefNode = null; - if (tagNs.equals(XDslKeys.DEFAULT.NS) && !parentTag.isInXDslXDef()) { + if (XDslKeys.DEFAULT.NS.equals(tagNs) && !parentTag.isInXDslXDef()) { schemaDefNode = xdslDefNode; selfDefNode = null; } // else if (parentSchemaDefNode != null) { // 在元元模型中,以 xdef 为名字空间的标签, // 需以 meta:unknown-tag 作为其节点定义,即,交叉定义 - if (parentTag.isInXDefXDef() && tagNs.equals(XDefKeys.DEFAULT.NS)) { + if (parentTag.isInXDefXDef() && XDefKeys.DEFAULT.NS.equals(tagNs)) { schemaDefNode = parentSchemaDefNode.getXdefUnknownTag(); } // 其余的,则将标签的 xdef 名字空间固定为名字 xdef diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java index 1df0ffca2..d8f0881f8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java @@ -63,7 +63,7 @@ public class XLangTextToken extends XmlTokenImpl { } String text = getText(); - XDefTypeDecl xdefValue = tag.getDefNodeXdefValue(); + XDefTypeDecl xdefValue = tag.getSchemaDefNodeXdefValue(); if (xdefValue == null || StringHelper.isBlank(text)) { return PsiReference.EMPTY_ARRAY; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java index c8c0c7289..bd1258ac5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java @@ -13,10 +13,9 @@ import java.util.function.Function; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; -import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlTag; import io.nop.api.core.util.SourceLocation; import io.nop.idea.plugin.lang.psi.XLangAttribute; +import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xdef.IXDefAttribute; @@ -49,12 +48,12 @@ public class XLangDefAttrReference extends XLangReferenceBase { SourceLocation loc = attrDef.getLocation(); Function targetResolver = (file) -> { - PsiElement target = XmlPsiHelper.getPsiElementAt(file, loc, XmlAttribute.class); + PsiElement target = XmlPsiHelper.getPsiElementAt(file, loc, XLangAttribute.class); if (target == null) { - target = XmlPsiHelper.getPsiElementAt(file, loc, XmlTag.class); + target = XmlPsiHelper.getPsiElementAt(file, loc, XLangTag.class); - if (target instanceof XmlTag tag) { + if (target instanceof XLangTag tag) { // Note: 在交叉定义时,属性定义中的属性名字与当前属性名字是不相同的 target = tag.getAttribute(attrDef.getName()); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java index 89c2e2ced..4a1953f9f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java @@ -11,8 +11,8 @@ package io.nop.idea.plugin.lang.reference; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.psi.XLangAttribute; import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.messages.NopPluginBundle; import org.jetbrains.annotations.Nullable; @@ -38,7 +38,7 @@ public class XLangParentTagAttrReference extends XLangReferenceBase { return null; } - XmlAttribute target = tag.getAttribute(attrValue); + XLangAttribute target = (XLangAttribute) tag.getAttribute(attrValue); if (target == null) { String msg = NopPluginBundle.message("xlang.annotation.reference.parent-tag-attr-not-found", attrValue); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java index 042d18269..93f3c9d1a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java @@ -13,7 +13,6 @@ import java.util.function.Function; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; -import com.intellij.psi.xml.XmlTag; import io.nop.api.core.util.SourceLocation; import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -44,7 +43,9 @@ public class XLangTagReference extends XLangReferenceBase { } SourceLocation loc = defNode.getLocation(); - Function targetResolver = (file) -> XmlPsiHelper.getPsiElementAt(file, loc, XmlTag.class); + Function targetResolver = (file) -> XmlPsiHelper.getPsiElementAt(file, + loc, + XLangTag.class); return new NopVirtualFile(myElement, path, targetResolver); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 3bad3998a..e83183af3 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -261,11 +261,8 @@ public class XmlPsiHelper { || elementType == XmlTokenType.XML_COMMENT_CHARACTERS; } - public static boolean isElementType(PsiElement elm, IElementType type) { - if (elm == null) { - return false; - } - ASTNode node = elm.getNode(); + public static boolean isElementType(PsiElement element, IElementType type) { + ASTNode node = element != null ? element.getNode() : null; if (node == null) { return false; } diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index 1b0b9934a..e3485d3b7 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -93,8 +93,8 @@ - + diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 2ff8056d1..797e1c382 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -162,6 +162,10 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur return ResourceHelper.readText(res); } + protected String insertCaretToVfs(String resource, String insertAt, String replacement) { + return readVfsResource(resource).replace(insertAt, replacement); + } + protected PsiElement getElementAtCaret() { assertCaretExists(); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index 6f7fa0b83..ffcb35a3a 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -11,7 +11,7 @@ package io.nop.idea.plugin.doc; import java.util.function.Consumer; import io.nop.idea.plugin.BaseXLangPluginTestCase; -import io.nop.xlang.xdef.XDefConstants; +import junit.framework.TestCase; /** * 参考 https://github.com/JetBrains/intellij-community/blob/master/xml/tests/src/com/intellij/html/HtmlDocumentationTest.java @@ -21,122 +21,416 @@ import io.nop.xlang.xdef.XDefConstants; */ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { - public void testGenerateDocForXmlName() { - // 显示标签文档 - doTest(""" - ple xmlns:x="/nop/schema/xdsl.xdef" x:schema="/test/doc/example.xdef"> - -
- """, "

example



This is root node

\n"); - doTest(""" - - ild name="Child"/> - - """, "

child



This is child node

\n"); - // 显示属性文档 - // - 确定属性 - doTest(""" - - me="Child"/> - - """, "

name

stdDomain: string



This is child name

\n"); - doTest(""" - - pe="leaf"/> - - """, "

type

stdDomain: dict:test/doc/child-type

"); - // - 未确定属性 - doTest(""" - - e="22"/> - - """, "

age

stdDomain: any



This a unknown attribute

\n"); - - // 显示 xdef.xdef 中的 meta:xxx 标签和属性文档 - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("nown-tag "), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", - "meta:ref=\"XDefNode\""), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("xmlns:meta", "xmlns:meta"), (genDoc) -> { - assertFalse(genDoc.contains("
")); - assertTrue(genDoc.contains("xmlns:meta")); - assertTrue(genDoc.contains(XDefConstants.STD_DOMAIN_XDEF_REF)); - }); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("own-tag "), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("-tag meta:ref="), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("ef="), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unknown-attr=\"string\"", - "meta:unknown-attr=\"string\""), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), - (genDoc) -> assertFalse(genDoc.contains("
"))); - - // 显示 xdsl.xdef 中的标签和 meta:xxx 属性文档 - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("nown-tag "), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema", "xdsl:schema"), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("value="), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(readVfsResource("/nop/schema/xdsl.xdef").replace("-parse "), - (genDoc) -> assertTrue(genDoc.contains("
"))); - - // 显示 xpl 类型子节点文档 - doTest(readVfsResource("/test/doc/example.xdef").replace("xpl:dump=\"true\"", "xpl:dump=\"true\""), - (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(""" - - - b="/nop/core/xlib/meta-gen.xlib"/> - - - """, (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(""" - - - b="/nop/core/xlib/meta-gen.xlib"/> - - - """, (genDoc) -> assertTrue(genDoc.contains("
"))); - doTest(""" - - - - - b="/nop/core/xlib/meta-gen.xlib"/> - - - - - """, (genDoc) -> assertTrue(genDoc.contains("
"))); + public void testGenerateDocForTag() { + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "own-tag x:schema"), // + (doc) -> { + assertTrue(doc.contains("自举定义")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "ta:unknown-tag meta:ref"), // + (doc) -> { + assertTrue(doc.contains("不会匹配xdef:unknown-tag")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "ine meta:name"), // + TestCase::assertNull // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "ef:unknown-tag meta:ref"), // + (doc) -> { + assertTrue(doc.contains("所有属性和节点都必须明确声明")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "fine xdef:name"), // + (doc) -> { + assertTrue(doc.contains("定义xdef片段")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "ef:prop name"), // + (doc) -> { + assertTrue(doc.contains("扩展属性")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // + "parse xdef:value"), // + (doc) -> { + assertTrue(doc.contains("之后执行此回调函数")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // + "def:unknown-tag xdsl:schema"), // + (doc) -> { + assertTrue(doc.contains("只在合并过程中存在")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // + "nown-tag xdef:value"), // + (doc) -> { + assertFalse(doc.contains("
")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "
", // + "st-parse>"), // + (doc) -> { + assertTrue(doc.contains("xdef:post-parse")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "ef:unknown-tag"), // + (doc) -> { + assertTrue(doc.contains("Any child node")); + assertFalse(doc.contains("/test/doc/example.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "ild name"), // + (doc) -> { + assertTrue(doc.contains("This is child node")); + assertFalse(doc.contains("/test/doc/example.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "", // + "mple>"), // + (doc) -> { + assertTrue(doc.contains("This is root node")); + assertFalse(doc.contains("/test/doc/example.xdef")); + } // + ); + + assertDoc(""" + ple xmlns:x="/nop/schema/xdsl.xdef" x:schema="/test/doc/example.xdef"> + + + """, // + (doc) -> { + assertTrue(doc.contains("This is root node")); + assertTrue(doc.contains("/test/doc/example.xdef")); + } // + ); + assertDoc(""" + + ild name="Child"/> + + """, // + (doc) -> { + assertTrue(doc.contains("This is child node")); + assertTrue(doc.contains("/test/doc/example.xdef")); + } // + ); + assertDoc(""" + + def name="Abc Def"/> + + """, // + (doc) -> { + assertTrue(doc.contains("Any child node")); + assertTrue(doc.contains("/test/doc/example.xdef")); + } // + ); + } + + public void testGenerateDocForAttribute() { + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "xmlns:meta", // + "xmlns:meta"), // + TestCase::assertNull // + ); + + // xdef.xdef 中的 meta:xxx 属性显示相应的 xdef:xxx 属性文档 + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "meta:check-ns=\"xdef\"", // + "meta:check-ns=\"xdef\""), // + (doc) -> { + assertTrue(doc.contains("必须要校验的名字空间")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "", // + "f=\"XDefNode\"/>"), // + (doc) -> { + assertTrue(doc.contains("引用本文件中")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "", // + "eta:ref=\"XDefNode\"/>"), // + (doc) -> { + assertTrue(doc.contains("引用本文件中")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "meta:unknown-attr=\"!xdef-attr\"", // + "meta:unknown-attr=\"!xdef-attr\""), // + (doc) -> { + assertTrue(doc.contains("具有未明确定义")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "meta:unknown-attr=\"string\"", // + "meta:unknown-attr=\"string\""), // + (doc) -> { + assertTrue(doc.contains("具有未明确定义")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "ta:value"), // + (doc) -> { + assertTrue(doc.contains("body的数据类型")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + // *.xdef 中,xdef/x/xpl 名字空间的属性,始终显示其定义的文档 + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "xdef:check-ns=\"word-set\"", // + "xdef:check-ns=\"word-set\""), // + (doc) -> { + assertTrue(doc.contains("必须要校验的名字空间")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "xdef:name=\"var-name\"", // + "xdef:name=\"var-name\""), // + (doc) -> { + assertTrue(doc.contains("注册为xdef片段")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + // - Note: xdef:define 节点上的 xdef:name 属性与 meta:define 节点上的 xdef:name 共享文档 + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "def:name=\"!var-name\""), // + (doc) -> { + assertTrue(doc.contains("注册为xdef片段")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // + "xdef:check-ns=\"x\"", // + "xdef:check-ns=\"x\""), // + (doc) -> { + assertTrue(doc.contains("必须要校验的")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // + "xdef:allow-multiple=\"true\"", // + "xdef:allow-multiple=\"true\""), // + (doc) -> { + assertTrue(doc.contains("允许多个实例")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // + "xdsl:schema", // + "xdsl:schema"), // + (doc) -> { + assertTrue(doc.contains("元模型文件路径")); + assertTrue(doc.contains("/nop/schema/xdsl.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // + "x:schema", // + "x:schema"), // + (doc) -> { + assertTrue(doc.contains("元模型文件路径")); + assertTrue(doc.contains("/nop/schema/xdsl.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // + "x:key-attr=\"xml-name\"", // + "x:key-attr=\"xml-name\""), // + (doc) -> { + assertTrue(doc.contains("子节点唯一属性")); + assertTrue(doc.contains("/nop/schema/xdsl.xdef")); + } // + ); + + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "", // + "-attr=\"any\"/>"), // + (doc) -> { + assertTrue(doc.contains("未明确定义")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "", // + "ef:value=\"v-path-list\"/>"), // + (doc) -> { + assertTrue(doc.contains("body的数据类型")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "x:dump=\"true\"", // + "x:dump=\"true\""), // + (doc) -> { + assertTrue(doc.contains("是否打印合并结果")); + assertTrue(doc.contains("/nop/schema/xdsl.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "xpl:dump=\"true\"", // + "xpl:dump=\"true\""), // + (doc) -> { + assertTrue(doc.contains("输出标签的AST树")); + assertTrue(doc.contains("/nop/schema/xpl.xdef")); + } // + ); + + assertDoc(""" + + + b="/nop/core/xlib/meta-gen.xlib"/> + + + """, // + (doc) -> { + assertTrue(doc.contains("引入标签库")); + assertTrue(doc.contains("/nop/schema/xpl.xdef")); + } // + ); + + // *.xdef 中,非 xdef/x/xpl 名字空间的属性,显示其自身的文档 + assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // + "me=\"!xml-name\""), // + (doc) -> { + assertFalse(doc.contains("具有未明确定义")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretToVfs("/test/doc/example.xdef", // + "me=\"string\""), // + (doc) -> { + assertTrue(doc.contains("This is child name")); + assertFalse(doc.contains("/test/doc/example.xdef")); + } // + ); + + // 普通 dsl 中,显示属性定义的文档 + // - 明确的属性 + assertDoc(""" + + me="Child"/> + + """, // + (doc) -> { + assertTrue(doc.contains("This is child name")); + assertTrue(doc.contains("/test/doc/example.xdef")); + } // + ); + assertDoc(""" + + pe="leaf"/> + + """, // + (doc) -> { + assertTrue(doc.contains("dict:test/doc/child-type")); + assertTrue(doc.contains("/test/doc/example.xdef")); + } // + ); + // - 未明确的属性 + assertDoc(""" + + e="22"/> + + """, // + (doc) -> { + assertTrue(doc.contains("This a unknown attribute")); + assertTrue(doc.contains("/test/doc/example.xdef")); + } // + ); + + assertDoc(""" + + + b="/nop/core/xlib/meta-gen.xlib"/> + + + """, // + (doc) -> { + assertTrue(doc.contains("引入标签库")); + assertTrue(doc.contains("/nop/schema/xpl.xdef")); + } // + ); + assertDoc(""" + + + + + b="/nop/core/xlib/meta-gen.xlib"/> + + + + + """, // + (doc) -> { + assertTrue(doc.contains("引入标签库")); + assertTrue(doc.contains("/nop/schema/xpl.xdef")); + } // + ); } public void testGenerateDocForXmlAttributeValue() { - doTest(""" - - - - """, "

leaf - Leaf Node

"); + assertDoc(""" + + + + """, "

leaf - Leaf Node

"); } - private void doTest(String text, String doc) { - doTest(text, (genDoc) -> assertEquals(doc, genDoc)); + private void assertDoc(String text, String doc) { + assertDoc(text, (genDoc) -> assertEquals(doc, genDoc)); } /** 通过在 text 中插入 <caret> 代表光标位置 */ - private void doTest(String text, Consumer checker) { + private void assertDoc(String text, Consumer checker) { configureByXLangText(text); String genDoc = getDocAtCaret(); -- Gitee From 92a124f2ae80ad4bac5b77bd34ae616c4dd13041 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sat, 19 Jul 2025 20:02:27 +0800 Subject: [PATCH 67/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B=20XL?= =?UTF-8?q?ang=20=E5=B1=9E=E6=80=A7=E5=80=BC=E6=96=87=E6=A1=A3=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=80=BB=E8=BE=91=EF=BC=8C=E5=B9=B6=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc/XLangDocumentationProvider.java | 58 +++-- .../idea/plugin/lang/XLangDocumentation.java | 135 +++++++++-- .../plugin/resource/ProjectDictProvider.java | 5 +- .../messages/NopPluginBundle.properties | 2 + .../messages/NopPluginBundle_zh.properties | 2 + .../idea/plugin/BaseXLangPluginTestCase.java | 2 +- .../doc/TestXLangDocumentationProvider.java | 225 +++++++++--------- .../test/resources/_vfs/test/doc/example.xdoc | 2 +- 8 files changed, 272 insertions(+), 159 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java index 44b2457bd..a09217f52 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java @@ -9,20 +9,21 @@ package io.nop.idea.plugin.doc; import java.util.Objects; +import com.intellij.codeInsight.documentation.DocumentationManagerProtocol; import com.intellij.lang.documentation.AbstractDocumentationProvider; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiManager; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.xml.XmlAttribute; import io.nop.api.core.beans.DictBean; import io.nop.api.core.beans.DictOptionBean; import io.nop.core.dict.DictProvider; import io.nop.idea.plugin.lang.XLangDocumentation; import io.nop.idea.plugin.lang.psi.XLangAttribute; import io.nop.idea.plugin.lang.psi.XLangTag; -import io.nop.idea.plugin.utils.MarkdownHelper; -import io.nop.idea.plugin.utils.XDefPsiHelper; -import io.nop.idea.plugin.utils.XmlTagInfo; +import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.xlang.xdef.IXDefAttribute; +import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; import jakarta.annotation.Nullable; @@ -50,19 +51,29 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { return null; } + XLangDocumentation doc = null; IElementType elementType = srcElement.getNode().getElementType(); if (elementType == XML_NAME) { - return generateDocForXmlName(srcElement); + doc = generateDocForXmlName(srcElement); } // else if (elementType == XML_ATTRIBUTE_VALUE_TOKEN) { - return generateDocForXmlAttributeValue(srcElement); + doc = generateDocForXmlAttributeValue(srcElement); } - return null; + return doc != null ? doc.genDoc() : null; + } + + /** + * 为文档链接中的 {@link DocumentationManagerProtocol#PSI_ELEMENT_PROTOCOL} + * 协议路径创建对应的 {@link PsiElement} + */ + @Override + public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) { + return XLangDocumentation.createElementForLink(context, link); } /** 为 xml 标签名和属性名生成文档 */ - private String generateDocForXmlName(PsiElement element) { + private XLangDocumentation generateDocForXmlName(PsiElement element) { PsiElement parent = element.getParent(); XLangDocumentation doc = null; @@ -76,25 +87,27 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { doc = tag != null ? tag.getAttrDocumentation(attrName) : null; } - return doc != null ? doc.toString() : null; + return doc; } /** 为 xml 属性值生成文档 */ - private String generateDocForXmlAttributeValue(PsiElement element) { - XmlAttribute attr = PsiTreeUtil.getParentOfType(element, XmlAttribute.class); + private XLangDocumentation generateDocForXmlAttributeValue(PsiElement element) { + XLangAttribute attr = PsiTreeUtil.getParentOfType(element, XLangAttribute.class); if (attr == null) { return null; } - String attrName = attr.getName(); - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(attr); - XDefTypeDecl attrDefType = tagInfo != null ? tagInfo.getDefAttrType(attrName) : null; - // TODO 显示类型定义文档 - if (attrDefType == null || attrDefType.getOptions() == null) { + IXDefAttribute attrDef = attr.getDefAttr(); + XDefTypeDecl attrDefType = attrDef != null ? attrDef.getType() : null; + if (attrDefType == null) { return null; } - DictBean dictBean = DictProvider.instance().getDict(null, attrDefType.getOptions(), null, null); + if (!XDefConstants.STD_DOMAIN_DICT.equals(attrDefType.getStdDomain())) { + return null; + } + + DictBean dictBean = loadDict(element, attrDefType.getOptions()); DictOptionBean option = dictBean != null ? dictBean.getOptionByValue(attr.getValue()) : null; if (option == null) { return null; @@ -111,14 +124,11 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { } doc.setDesc(option.getDescription()); - return doc.toString(); + return doc; } - /** 对于多行文本,行首的 > 将被去除后,再按照 markdown 渲染得到 html 代码 */ - public static String markdown(String text) { - text = text.replaceAll("(?m)^> ", ""); - text = MarkdownHelper.renderHtml(text); - - return text; + private DictBean loadDict(PsiElement element, String dictName) { + return ProjectEnv.withProject(element.getProject(), + () -> DictProvider.instance().getDict(null, dictName, null, null)); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java index 59aa9518e..13f84776e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java @@ -8,13 +8,25 @@ package io.nop.idea.plugin.lang; +import com.intellij.codeInsight.documentation.DocumentationManagerProtocol; +import com.intellij.codeInsight.javadoc.JavaDocHighlightingManagerImpl; +import com.intellij.lang.documentation.DocumentationMarkup; +import com.intellij.lang.documentation.DocumentationSettings; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.editor.richcopy.HtmlSyntaxInfoUtil; +import com.intellij.openapi.util.text.HtmlChunk; +import com.intellij.psi.PsiElement; import io.nop.api.core.util.ISourceLocationGetter; import io.nop.commons.util.StringHelper; -import io.nop.idea.plugin.doc.XLangDocumentationProvider; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.MarkdownHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.XDefTypeDecl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * XLang 说明文档 @@ -23,10 +35,12 @@ import io.nop.xlang.xdef.XDefTypeDecl; * @date 2025-07-18 */ public class XLangDocumentation { + private static final String LINK_PREFIX_NOP_VFS = "nop-vfs:"; + String mainTitle; String subTitle; String stdDomain; - boolean required; + Boolean required; String desc; String path; @@ -66,37 +80,108 @@ public class XLangDocumentation { this.desc = desc; } - @Override - public String toString() { + public String genDoc() { + JavaDocHighlightingManagerImpl highlightingManager = JavaDocHighlightingManagerImpl.getInstance(); + StringBuilder sb = new StringBuilder(); + sb.append(""); - sb.append("

"); - sb.append(StringHelper.escapeXml(this.mainTitle)); - if (StringHelper.isNotBlank(this.subTitle)) { - sb.append(" - ").append(StringHelper.escapeXml(this.subTitle)); - } - sb.append("

"); - - if (this.stdDomain != null) { - sb.append("

"); - sb.append("stdDomain: "); - sb.append(this.required ? "[Required] " : "[Option] "); - sb.append("").append(StringHelper.escapeXml(this.stdDomain)).append(""); - sb.append("

"); + if (path != null) { + String raw = createElementLink(LINK_PREFIX_NOP_VFS + path, path); + + HtmlChunk html = HtmlChunk.div().setClass("bottom") // + .children( // + HtmlChunk.tag("icon").attr("src", "AllIcons.Nodes.Package"), + HtmlChunk.nbsp(), + HtmlChunk.raw(raw)); + html.appendTo(sb); } - if (this.path != null) { - sb.append("

"); - sb.append("vfs: "); - sb.append("").append(StringHelper.escapeXml(this.path)).append(""); - sb.append("

"); + { + sb.append(DocumentationMarkup.DEFINITION_START); + + appendStyledSpan(sb, highlightingManager.getKeywordAttributes(), mainTitle); + if (StringHelper.isNotBlank(subTitle)) { + sb.append(" - "); + appendStyledSpan(sb, highlightingManager.getClassNameAttributes(), subTitle); + } + + StringBuilder sb1 = new StringBuilder(); + if (required != null) { + appendStyledSpan(sb1, + highlightingManager.getInstanceFieldAttributes(), + required + ? NopPluginBundle.message("xlang.doc.flag.required") + : NopPluginBundle.message("xlang.doc.flag.option")); + } + if (stdDomain != null) { + if (!sb1.isEmpty()) { + sb1.append(' '); + } + appendStyledSpan(sb1, highlightingManager.getLocalVariableAttributes(), stdDomain); + } + if (!sb1.isEmpty()) { + sb.append("\n\n").append(sb1); + } + + sb.append(DocumentationMarkup.DEFINITION_END); } - if (!StringHelper.isBlank(this.desc)) { - sb.append("

"); - sb.append(XLangDocumentationProvider.markdown(this.desc)); + if (!StringHelper.isBlank(desc)) { + String raw = markdown(desc); + + sb.append(DocumentationMarkup.CONTENT_START); + sb.append(raw); + sb.append(DocumentationMarkup.CONTENT_END); } + sb.append(""); + return !sb.isEmpty() ? sb.toString() : null; } + + public static PsiElement createElementForLink(PsiElement context, String link) { + if (link.startsWith(LINK_PREFIX_NOP_VFS)) { + String path = link.substring(LINK_PREFIX_NOP_VFS.length()); + + return new NopVirtualFile(context, path); + } + return null; + } + + /** 对于多行文本,行首的 > 将被去除后,再按照 markdown 渲染得到 html 代码 */ + private static String markdown(String text) { + text = text.replaceAll("(?m)^> ", ""); + text = MarkdownHelper.renderHtml(text); + + return text; + } + + // <<<<<<<<<<<<<<< Source from com.intellij.lang.java.JavaDocumentationProvider + private static String createElementLink(String path, String text) { + // 参考 com.intellij.codeInsight.documentation.DocumentationManagerUtil#createHyperlinkImpl + return "" + + "" + + text + + ""; + } + + private static void appendStyledSpan( + @NotNull StringBuilder sb, @Nullable String value, String @NotNull ... properties + ) { + HtmlSyntaxInfoUtil.appendStyledSpan(sb, StringHelper.escapeXml(value), properties); + } + + private static void appendStyledSpan( + @NotNull StringBuilder sb, @NotNull TextAttributes attributes, @Nullable String value + ) { + HtmlSyntaxInfoUtil.appendStyledSpan(sb, + attributes, + StringHelper.escapeXml(value), + DocumentationSettings.getHighlightingSaturation(false)); + } + // >>>>>>>>>>>>>>>>>>>>>>> } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java index dd1035c95..b39282fce 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/ProjectDictProvider.java @@ -65,7 +65,10 @@ public class ProjectDictProvider implements IDictProvider { if (resource.exists()) { ResourceComponentManager.instance().traceDepends(resource.getPath()); - return new DictModelParser().parseFromResource(resource); + DictModel dictModel = new DictModelParser().parseFromResource(resource); + dictModel.getDictBean().setLocation(dictModel.getLocation()); + + return dictModel; } } // 从枚举类中得到字典信息 diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties index db2031b7b..ac3eb76d2 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties @@ -19,5 +19,7 @@ xlang.annotation.reference.x-prototype-attr-not-found = No sibling node which ha xlang.annotation.reference.attr-xdef-not-defined = Undefined attribute ''{0}'' xlang.annotation.reference.std-domain-not-registered = The std-domain ''{0}'' isn''t listed in {1} xlang.annotation.reference.xdef-name-class-not-found = The Java class ''{0}'' isn''t created or generated +xlang.doc.flag.required = [Required] +xlang.doc.flag.option = [Option] xlang.doc.markdown.link-title = '{'Link'}'{0} xlang.doc.markdown.image-title = '{'Image'}'{0} diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties index 5f455d8f4..55adc4f16 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties @@ -19,5 +19,7 @@ xlang.annotation.reference.x-prototype-attr-not-found = \u4E0D\u5B58\u5728\u5C5E xlang.annotation.reference.attr-xdef-not-defined = \u5C5E\u6027 ''{0}'' \u672A\u5B9A\u4E49 xlang.annotation.reference.std-domain-not-registered = \u6570\u636E\u57DF ''{0}'' \u4E0D\u5728\u53EF\u9009\u5217\u8868 {1} \u4E2D xlang.annotation.reference.xdef-name-class-not-found = \u8FD8\u672A\u521B\u5EFA\u6216\u751F\u6210 Java \u7C7B ''{0}'' +xlang.doc.flag.required = [\u5FC5\u586B] +xlang.doc.flag.option = [\u53EF\u9009] xlang.doc.markdown.link-title = '{'\u94FE\u63A5'}'{0} xlang.doc.markdown.image-title = '{'\u56FE\u7247'}'{0} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 797e1c382..054cf9241 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -162,7 +162,7 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur return ResourceHelper.readText(res); } - protected String insertCaretToVfs(String resource, String insertAt, String replacement) { + protected String insertCaretIntoVfs(String resource, String insertAt, String replacement) { return readVfsResource(resource).replace(insertAt, replacement); } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index ffcb35a3a..c419b387e 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -22,104 +22,104 @@ import junit.framework.TestCase; public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { public void testGenerateDocForTag() { - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "own-tag x:schema"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "own-tag x:schema"), // (doc) -> { assertTrue(doc.contains("自举定义")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "ta:unknown-tag meta:ref"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ta:unknown-tag meta:ref"), // (doc) -> { assertTrue(doc.contains("不会匹配xdef:unknown-tag")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "ine meta:name"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ine meta:name"), // TestCase::assertNull // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "ef:unknown-tag meta:ref"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ef:unknown-tag meta:ref"), // (doc) -> { assertTrue(doc.contains("所有属性和节点都必须明确声明")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "fine xdef:name"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "fine xdef:name"), // (doc) -> { assertTrue(doc.contains("定义xdef片段")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "ef:prop name"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ef:prop name"), // (doc) -> { assertTrue(doc.contains("扩展属性")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // - "parse xdef:value"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "parse xdef:value"), // (doc) -> { assertTrue(doc.contains("之后执行此回调函数")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // - "def:unknown-tag xdsl:schema"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "def:unknown-tag xdsl:schema"), // (doc) -> { assertTrue(doc.contains("只在合并过程中存在")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // - "nown-tag xdef:value"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "nown-tag xdef:value"), // (doc) -> { assertFalse(doc.contains("
")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "", // - "st-parse>"), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "", // + "st-parse>"), // (doc) -> { assertTrue(doc.contains("xdef:post-parse")); assertTrue(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "ef:unknown-tag"), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "ef:unknown-tag"), // (doc) -> { assertTrue(doc.contains("Any child node")); assertFalse(doc.contains("/test/doc/example.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "ild name"), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "ild name"), // (doc) -> { assertTrue(doc.contains("This is child node")); assertFalse(doc.contains("/test/doc/example.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "", // - "mple>"), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "", // + "mple>"), // (doc) -> { assertTrue(doc.contains("This is root node")); assertFalse(doc.contains("/test/doc/example.xdef")); @@ -159,56 +159,56 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { } public void testGenerateDocForAttribute() { - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "xmlns:meta", // - "xmlns:meta"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "xmlns:meta", // + "xmlns:meta"), // TestCase::assertNull // ); // xdef.xdef 中的 meta:xxx 属性显示相应的 xdef:xxx 属性文档 - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "meta:check-ns=\"xdef\"", // - "meta:check-ns=\"xdef\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:check-ns=\"xdef\"", // + "meta:check-ns=\"xdef\""), // (doc) -> { assertTrue(doc.contains("必须要校验的名字空间")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "", // - "f=\"XDefNode\"/>"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "", // + "f=\"XDefNode\"/>"), // (doc) -> { assertTrue(doc.contains("引用本文件中")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "", // - "eta:ref=\"XDefNode\"/>"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "", // + "eta:ref=\"XDefNode\"/>"), // (doc) -> { assertTrue(doc.contains("引用本文件中")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "meta:unknown-attr=\"!xdef-attr\"", // - "meta:unknown-attr=\"!xdef-attr\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:unknown-attr=\"!xdef-attr\"", // + "meta:unknown-attr=\"!xdef-attr\""), // (doc) -> { assertTrue(doc.contains("具有未明确定义")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "meta:unknown-attr=\"string\"", // - "meta:unknown-attr=\"string\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:unknown-attr=\"string\"", // + "meta:unknown-attr=\"string\""), // (doc) -> { assertTrue(doc.contains("具有未明确定义")); assertTrue(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "ta:value"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ta:value"), // (doc) -> { assertTrue(doc.contains("body的数据类型")); assertTrue(doc.contains("/nop/schema/xdef.xdef")); @@ -216,102 +216,102 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { ); // *.xdef 中,xdef/x/xpl 名字空间的属性,始终显示其定义的文档 - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "xdef:check-ns=\"word-set\"", // - "xdef:check-ns=\"word-set\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "xdef:check-ns=\"word-set\"", // + "xdef:check-ns=\"word-set\""), // (doc) -> { assertTrue(doc.contains("必须要校验的名字空间")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "xdef:name=\"var-name\"", // - "xdef:name=\"var-name\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "xdef:name=\"var-name\"", // + "xdef:name=\"var-name\""), // (doc) -> { assertTrue(doc.contains("注册为xdef片段")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); // - Note: xdef:define 节点上的 xdef:name 属性与 meta:define 节点上的 xdef:name 共享文档 - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "def:name=\"!var-name\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "def:name=\"!var-name\""), // (doc) -> { assertTrue(doc.contains("注册为xdef片段")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // - "xdef:check-ns=\"x\"", // - "xdef:check-ns=\"x\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdef:check-ns=\"x\"", // + "xdef:check-ns=\"x\""), // (doc) -> { assertTrue(doc.contains("必须要校验的")); assertTrue(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // - "xdef:allow-multiple=\"true\"", // - "xdef:allow-multiple=\"true\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdef:allow-multiple=\"true\"", // + "xdef:allow-multiple=\"true\""), // (doc) -> { assertTrue(doc.contains("允许多个实例")); assertTrue(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // - "xdsl:schema", // - "xdsl:schema"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdsl:schema", // + "xdsl:schema"), // (doc) -> { assertTrue(doc.contains("元模型文件路径")); assertTrue(doc.contains("/nop/schema/xdsl.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // - "x:schema", // - "x:schema"), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "x:schema", // + "x:schema"), // (doc) -> { assertTrue(doc.contains("元模型文件路径")); assertTrue(doc.contains("/nop/schema/xdsl.xdef")); } // ); - assertDoc(insertCaretToVfs("/nop/schema/xdsl.xdef", // - "x:key-attr=\"xml-name\"", // - "x:key-attr=\"xml-name\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "x:key-attr=\"xml-name\"", // + "x:key-attr=\"xml-name\""), // (doc) -> { assertTrue(doc.contains("子节点唯一属性")); assertTrue(doc.contains("/nop/schema/xdsl.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "", // - "-attr=\"any\"/>"), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "", // + "-attr=\"any\"/>"), // (doc) -> { assertTrue(doc.contains("未明确定义")); assertTrue(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "", // - "ef:value=\"v-path-list\"/>"), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "", // + "ef:value=\"v-path-list\"/>"), // (doc) -> { assertTrue(doc.contains("body的数据类型")); assertTrue(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "x:dump=\"true\"", // - "x:dump=\"true\""), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "x:dump=\"true\"", // + "x:dump=\"true\""), // (doc) -> { assertTrue(doc.contains("是否打印合并结果")); assertTrue(doc.contains("/nop/schema/xdsl.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "xpl:dump=\"true\"", // - "xpl:dump=\"true\""), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "xpl:dump=\"true\"", // + "xpl:dump=\"true\""), // (doc) -> { assertTrue(doc.contains("输出标签的AST树")); assertTrue(doc.contains("/nop/schema/xpl.xdef")); @@ -334,17 +334,17 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { ); // *.xdef 中,非 xdef/x/xpl 名字空间的属性,显示其自身的文档 - assertDoc(insertCaretToVfs("/nop/schema/xdef.xdef", // - "me=\"!xml-name\""), // + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "me=\"!xml-name\""), // (doc) -> { assertFalse(doc.contains("具有未明确定义")); assertFalse(doc.contains("/nop/schema/xdef.xdef")); } // ); - assertDoc(insertCaretToVfs("/test/doc/example.xdef", // - "me=\"string\""), // + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "me=\"string\""), // (doc) -> { assertTrue(doc.contains("This is child name")); assertFalse(doc.contains("/test/doc/example.xdef")); @@ -418,15 +418,26 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { } public void testGenerateDocForXmlAttributeValue() { +// // TODO 暂时不显示属性类型的文档 +// assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // +// "ring\""), // +// (doc) -> { +// assertTrue(doc.contains("字符串类型")); +// assertTrue(doc.contains("/dict/core/std-domain.dict.yaml")); +// } // +// ); + assertDoc(""" - """, "

leaf - Leaf Node

"); - } - - private void assertDoc(String text, String doc) { - assertDoc(text, (genDoc) -> assertEquals(doc, genDoc)); + """, // + (doc) -> { + assertTrue(doc.contains("Leaf Node")); + assertTrue(doc.contains("/dict/test/doc/child-type.dict.yaml")); + } // + ); } /** 通过在 text 中插入 <caret> 代表光标位置 */ diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc index c78d94dbb..b8566f01e 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc @@ -1,7 +1,7 @@ - + /test/reference/test-filter.xdef, /nop/schema/xdsl.xdef -- Gitee From 7cab56ffda1794f99635085d07bcb3588b934225 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sat, 19 Jul 2025 22:07:13 +0800 Subject: [PATCH 68/82] =?UTF-8?q?nop-idea-pugin:=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E4=BB=A3=E7=A0=81=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nop/idea/plugin/lang/TestXLangParser.java | 3 +- .../idea/plugin/lang/TestXLangReferences.java | 730 ++++++++++++------ .../lang/TestXLangScriptCompletions.java | 54 +- .../plugin/lang/TestXLangScriptParser.java | 7 +- .../lang/TestXLangScriptReferences.java | 203 +++-- .../plugin/lang/TestXLangScriptRename.java | 490 +++++++----- 6 files changed, 993 insertions(+), 494 deletions(-) diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java index 7a1389608..26b65dcb0 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java @@ -34,7 +34,8 @@ public class TestXLangParser extends BaseXLangPluginTestCase {
""", // - "/test/ast/xlang-1.ast"); + "/test/ast/xlang-1.ast" // + ); } protected void assertASTTree(String code, String expectedAstFile) { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 1802a59cd..d403a36bf 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -25,57 +25,93 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { public void testTagDefReferences() { // 名字空间保持名字引用 - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("def:pre-parse"), - "xdef"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", "ta:define>"), - "meta"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "def:pre-parse"), // + "xdef" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "", // + "ta:define>"), // + "meta" // + ); // *.xdef 的根节点定义始终对应 xdef.xdef 的根节点 meta:unknown-tag, // 同时,在 xdef.xdef 中未定义的子节点也为对应的 meta:unknown-tag - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("own-tag x:schema"), - "/nop/schema/xdef.xdef?meta:unknown-tag"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("nown-tag xdsl:schema"), - "/nop/schema/xdef.xdef?meta:unknown-tag"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("-parse"), - "/nop/schema/xdef.xdef?meta:unknown-tag"); - assertReference(readVfsResource("/nop/schema/xpl.xdef").replace("", - " xdef:value=\"xpl\"/>"), - "/nop/schema/xdef.xdef?meta:unknown-tag"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "own-tag x:schema"), // + "/nop/schema/xdef.xdef?meta:unknown-tag" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "nown-tag xdsl:schema"), // + // + "/nop/schema/xdef.xdef?meta:unknown-tag" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "-parse"), // + "/nop/schema/xdef.xdef?meta:unknown-tag" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xpl.xdef", // + "", // + " xdef:value=\"xpl\"/>"), // + "/nop/schema/xdef.xdef?meta:unknown-tag" // + ); assertReference(""" mple> - """, "/nop/schema/xdef.xdef?meta:unknown-tag"); + """, // + "/nop/schema/xdef.xdef?meta:unknown-tag" // + ); // xdef.xdef 中的节点交叉定义 - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("parse"), - "/nop/schema/xdef.xdef?meta:unknown-tag"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("n-tag"), - "/nop/schema/xdef.xdef?meta:unknown-tag"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", "fine>"), - "/nop/schema/xdef.xdef?xdef:define"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", - "own-tag meta:ref=\"XDefNode\"/>"), - "/nop/schema/xdef.xdef?xdef:unknown-tag"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "parse"), // + "/nop/schema/xdef.xdef?meta:unknown-tag" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "n-tag"), // + "/nop/schema/xdef.xdef?meta:unknown-tag" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "", // + "fine>"), // + "/nop/schema/xdef.xdef?xdef:define" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef",// + "", // + "own-tag meta:ref=\"XDefNode\"/>"), // + "/nop/schema/xdef.xdef?xdef:unknown-tag" // + ); // 对 xdef.xdef 中同名子节点的定义引用 - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("", - "own-tag xdef:ref=\"DslNode\"/>"), - "/nop/schema/xdef.xdef?xdef:unknown-tag"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("nown-tag xdef:value"), - "/nop/schema/xdef.xdef?xdef:unknown-tag"); - - assertReference(readVfsResource("/nop/schema/xpl.xdef").replace("ine xdef:name"), - "/nop/schema/xdef.xdef?xdef:define"); - assertReference(readVfsResource("/nop/schema/xpl.xdef").replace("own-tag xpl:unknown-attr"), - "/nop/schema/xdef.xdef?xdef:unknown-tag"); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef",// + "", // + "own-tag xdef:ref=\"DslNode\"/>"), // + "/nop/schema/xdef.xdef?xdef:unknown-tag" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "nown-tag xdef:value"), // + "/nop/schema/xdef.xdef?xdef:unknown-tag" // + ); + + assertReference(insertCaretIntoVfs("/nop/schema/xpl.xdef", // + "ine xdef:name"), // + "/nop/schema/xdef.xdef?xdef:define" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xpl.xdef",// + "own-tag xpl:unknown-attr"), // + "/nop/schema/xdef.xdef?xdef:unknown-tag" // + ); // 普通 xdef 内的定义引用 assertReference(""" @@ -84,14 +120,18 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { > -parse/> - """, "/nop/schema/xdef.xdef?xdef:post-parse"); + """, // + "/nop/schema/xdef.xdef?xdef:post-parse" // + ); assertReference(""" ld name="string"/> - """, "/nop/schema/xdef.xdef?meta:unknown-tag"); + """, // + "/nop/schema/xdef.xdef?meta:unknown-tag" // + ); // DSL 内的定义引用 assertReference(""" @@ -99,28 +139,36 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { x:schema="/test/doc/example.xdef" > - """, "/test/doc/example.xdef?example"); + """, // + "/test/doc/example.xdef?example" // + ); assertReference(""" mple xmlns:x="/nop/schema/xdsl.xdef" x:schema="/test/doc/example.xdef" > extends/> - """, "/nop/schema/xdsl.xdef?x:gen-extends"); + """, // + "/nop/schema/xdsl.xdef?x:gen-extends" // + ); assertReference(""" ild name="Child"/> - """, "/test/doc/example.xdef?child"); + """, // + "/test/doc/example.xdef?child" // + ); assertReference(""" own name="abc"/> - """, "/test/doc/example.xdef?xdef:unknown-tag"); + """, // + "/test/doc/example.xdef?xdef:unknown-tag" // + ); assertReference(""" c name="abc"/> - """, null); + """, // + null // + ); assertReference(""" xtends/> - """, "/nop/schema/xdsl.xdef?x:gen-extends"); + """, // + "/nop/schema/xdsl.xdef?x:gen-extends" // + ); } public void testXplReferences() { @@ -153,7 +205,9 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { - """, "/nop/schema/xlib.xdef?source"); + """, // + "/nop/schema/xlib.xdef?source" // + ); assertReference(""" - """, "/nop/schema/xpl.xdef?xdef:unknown-tag"); + """, // + "/nop/schema/xpl.xdef?xdef:unknown-tag" // + ); // // TODO xpl 节点内引用内置的 xpl 标签函数 // assertReference(""" @@ -177,7 +233,9 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { // ort from="/test/reference/a.xlib"/> // // -// """, ""); +// """, // +// "" // +// ); // assertReference(""" // ipt> // // -// """, ""); +// """, // +// "" // +// ); // 通过 xpl:lib 导入 xlib assertReference(""" @@ -197,7 +257,9 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { ByMdxQuery xpl:lib="/test/reference/a.xlib"/> - """, "/test/reference/a.xlib#DoFindByMdxQuery"); + """, // + "/test/reference/a.xlib#DoFindByMdxQuery" // + ); assertReference(""" hod="post" xpl:lib="/test/reference/a.xlib"/> - """, "/test/reference/a.xlib#DoFindByMdxQuery"); + """, // + "/test/reference/a.xlib#DoFindByMdxQuery" // + ); // 通过 c:import 导入 xlib assertReference(""" @@ -218,7 +282,9 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { ByMdxQuery/> - """, "/test/reference/a.xlib#DoFindByMdxQuery"); + """, // + "/test/reference/a.xlib#DoFindByMdxQuery" // + ); assertReference(""" hod="post"/> - """, "/test/reference/a.xlib#DoFindByMdxQuery"); + """, // + "/test/reference/a.xlib#DoFindByMdxQuery" // + ); assertReference(""" ByMdxQuery/> - """, "/test/reference/a.xlib#DoFindByMdxQuery"); + """, // + "/test/reference/a.xlib#DoFindByMdxQuery" // + ); assertReference(""" hod="post"/> - """, "/test/reference/a.xlib#DoFindByMdxQuery"); + """, // + "/test/reference/a.xlib#DoFindByMdxQuery" // + ); } public void testAttributeReferences() { // xdef.xdef 属性的交叉定义识别 // - 名字空间引用其自身 - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), "meta"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("ef:name=\"!var-name\""), - "xdef"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:unique-attr=\"name\"", // + "meta:unique-attr=\"name\""), // + "meta" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef",// + "ef:name=\"!var-name\""), // + "xdef" // + ); // - 以 meta 为名字空间的属性(含 meta:unknown-attr)由对应的 xdef:xxx 定义 - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unknown-attr=\"!xdef-attr\"", - "meta:unknown-attr=\"!xdef-attr\""), - "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:bean-tag-prop=\"tagName\"", - "meta:bean-tag-prop=\"tagName\""), - "/nop/schema/xdef.xdef?meta:define#xdef:bean-tag-prop=prop-name"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:unknown-attr=\"!xdef-attr\"", // + "meta:unknown-attr=\"!xdef-attr\""), // + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:bean-tag-prop=\"tagName\"", // + "meta:bean-tag-prop=\"tagName\""), // + "/nop/schema/xdef.xdef?meta:define#xdef:bean-tag-prop=prop-name" // + ); // - 全部以 xdef 为名字空间的属性均由 meta:unknown-attr 定义 - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:unknown-attr=\"def-type\"", - "xdef:unknown-attr=\"def-type\""), - "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:unique-attr=\"xml-name\"", - "xdef:unique-attr=\"xml-name\""), - "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("xdef:ref=\"xdef-ref\"", - "xdef:ref=\"xdef-ref\""), - "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "xdef:unknown-attr=\"def-type\"", // + "xdef:unknown-attr=\"def-type\""), // + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "xdef:unique-attr=\"xml-name\"", // + "xdef:unique-attr=\"xml-name\""), // + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "xdef:ref=\"xdef-ref\"", // + "xdef:ref=\"xdef-ref\""), // + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr" // + ); // - xdef 或 meta 名字空间的节点属性,也满足以上规则 - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", - "f=\"XDefNode\"/>"), - "/nop/schema/xdef.xdef?meta:define#xdef:ref=xdef-ref"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("", - "f=\"XDefNode\"/>"), - "/nop/schema/xdef.xdef?meta:define#xdef:ref=xdef-ref"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"!xml-name\""), - "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unknown-attr=\"string\"", - "meta:unknown-attr=\"string\""), - "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("ame=\"!var-name\""), - "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("me=\"XDefNode\""), - "/nop/schema/xdef.xdef?xdef:define#xdef:name=!var-name"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef",// + "", // + "f=\"XDefNode\"/>"), // + "/nop/schema/xdef.xdef?meta:define#xdef:ref=xdef-ref" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef",// + "", // + "f=\"XDefNode\"/>"), // + "/nop/schema/xdef.xdef?meta:define#xdef:ref=xdef-ref" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "me=\"!xml-name\""), // + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:unknown-attr=\"string\"", // + "meta:unknown-attr=\"string\""), // + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef",// + "ame=\"!var-name\""), // + "/nop/schema/xdef.xdef?meta:define#meta:unknown-attr=!xdef-attr" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef",// + "me=\"XDefNode\""), // + "/nop/schema/xdef.xdef?xdef:define#xdef:name=!var-name" // + ); // xdsl.xdef 中的属性定义识别 // - 交叉识别 - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=", "xdsl:schema="), - "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:schema=v-path"); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdsl:schema=", // + "xdsl:schema="), // + "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:schema=v-path" // + ); // - 普通定义 - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("x:schema=\"v-path\"", - "x:schema=\"v-path\""), - "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:allow-multiple=\"true\"", - "xdef:allow-multiple=\"true\""), - "/nop/schema/xdef.xdef?meta:define#xdef:allow-multiple=boolean"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("x:key-attr=\"xml-name\"", - "x:key-attr=\"xml-name\""), - "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("", - "lue=\"xpl-node\"/>"), - "/nop/schema/xdef.xdef?meta:define#xdef:value=def-type"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("", - "ernal=\"true\"/>"), - "/nop/schema/xdef.xdef?meta:define#xdef:internal=boolean"); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "x:schema=\"v-path\"", // + "x:schema=\"v-path\""), // + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdef:allow-multiple=\"true\"", // + "xdef:allow-multiple=\"true\""), // + "/nop/schema/xdef.xdef?meta:define#xdef:allow-multiple=boolean" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "x:key-attr=\"xml-name\"", // + "x:key-attr=\"xml-name\""), // + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef",// + "", // + "lue=\"xpl-node\"/>"), // + "/nop/schema/xdef.xdef?meta:define#xdef:value=def-type" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef",// + "", // + "ernal=\"true\"/>"), // + "/nop/schema/xdef.xdef?meta:define#xdef:internal=boolean" // + ); // 普通 xdef 的属性定义识别 - assertReference(readVfsResource("/test/doc/example.xdef").replace("me=\"string\""), - "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type"); - assertReference(readVfsResource("/test/doc/example.xdef").replace("x:dump", "x:dump"), - "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:dump=boolean"); - assertReference(readVfsResource("/test/doc/example.xdef").replace("xpl:dump", "xpl:dump"), - "/nop/schema/xpl.xdef?xdef:define#xpl:dump=boolean"); + assertReference(insertCaretIntoVfs("/test/doc/example.xdef", // + "me=\"string\""), // + "/nop/schema/xdef.xdef?meta:define#xdef:unknown-attr=def-type" // + ); + assertReference(insertCaretIntoVfs("/test/doc/example.xdef", // + "x:dump", // + "x:dump"), // + "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:dump=boolean" // + ); + assertReference(insertCaretIntoVfs("/test/doc/example.xdef", // + "xpl:dump", // + "xpl:dump"), // + "/nop/schema/xpl.xdef?xdef:define#xpl:dump=boolean" // + ); assertReference(""" ract="true"/> - """, "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:abstract=boolean"); + """, // + "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:abstract=boolean" // + ); assertReference(""" pe="leaf"/> - """, "/test/doc/example.xdef?child#type=dict:test/doc/child-type=node"); + """, // + "/test/doc/example.xdef?child#type=dict:test/doc/child-type=node" // + ); assertReference(""" ge="22"/> - """, "/test/doc/example.xdef?child#xdef:unknown-attr=any"); + """, // + "/test/doc/example.xdef?child#xdef:unknown-attr=any" // + ); assertReference(""" ge="23"/> - """, "/test/doc/example.xdef?xdef:unknown-tag#xdef:unknown-attr=any"); + """, // + "/test/doc/example.xdef?xdef:unknown-tag#xdef:unknown-attr=any" // + ); assertReference(""" ="aaa"/> - """, null); + """, // + null // + ); assertReference(""" f="/test/reference/test-filter.xdef#FilterCondition" /> - """, "/nop/schema/schema/schema-node.xdef?schema#ref=xdef-ref"); + """, // + "/nop/schema/schema/schema-node.xdef?schema#ref=xdef-ref" // + ); // 对 Xpl 属性的引用识别 assertReference(""" @@ -378,7 +510,9 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { mp="true"/> - """, "/nop/schema/xpl.xdef?xdef:define#xpl:dump=boolean"); + """, // + "/nop/schema/xpl.xdef?xdef:define#xpl:dump=boolean" // + ); assertReference(""" """, - "/nop/schema/xpl.xdef?xdef:define#xpl:outputMode=enum:io.nop.xlang.ast.XLangOutputMode"); + "/nop/schema/xpl.xdef?xdef:define#xpl:outputMode=enum:io.nop.xlang.ast.XLangOutputMode" + // + ); assertReference(""" - """, null); + """, // + null // + ); assertReference(""" - """, "/nop/schema/xpl.xdef?xdef:define#xpl:if=expr"); + """, // + "/nop/schema/xpl.xdef?xdef:define#xpl:if=expr" // + ); assertReference(""" @@ -423,7 +563,9 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { - """, "/nop/schema/xpl.xdef?xdef:define#xpl:if=expr"); + """, // + "/nop/schema/xpl.xdef?xdef:define#xpl:if=expr" // + ); } public void testAttributeValueReferences() { @@ -433,20 +575,28 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { - """, "/nop/schema/xmeta.xdef"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("x:schema=\"/nop/schema/xdef.xdef\"", - "x:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdsl:schema=\"/nop/schema/xdef.xdef\"", - "xdsl:schema=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); + """, // + "/nop/schema/xmeta.xdef" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "x:schema=\"/nop/schema/xdef.xdef\"", // + "x:schema=\"/nop/schema/xdef.xdef\""), // + "/nop/schema/xdef.xdef" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef",// + "xdsl:schema=\"/nop/schema/xdef.xdef\"", // + "xdsl:schema=\"/nop/schema/xdef.xdef\""), // + "/nop/schema/xdef.xdef" // + ); // - xdef:default-extends=v-path assertReference("""
- """, "/test/reference/default.xform"); + """, // + "/test/reference/default.xform" // + ); // - xpl:lib=v-path assertReference(""" - """, "/nop/core/xlib/meta-gen.xlib"); + """, // + "/nop/core/xlib/meta-gen.xlib" // + ); // 对 v-path-list 列表元素的引用 assertReference(""" - """, "/test/reference/a.xmeta"); + """, // + "/test/reference/a.xmeta" // + ); assertReference(""" - """, "/test/reference/b.xmeta"); + """, // + "/test/reference/b.xmeta" // + ); // 对 xdef-ref 类型属性的引用 // - xmlns:xxx 默认为 xdef-ref 类型 assertReference(""" - """, "/nop/schema/xdsl.xdef"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xmlns:xdef=\"/nop/schema/xdef.xdef\"", - "xmlns:xdef=\"/nop/schema/xdef.xdef\""), - "/nop/schema/xdef.xdef"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xmlns:x=\"x\"", "xmlns:x=\"x\""), - null); + """, // + "/nop/schema/xdsl.xdef" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xmlns:xdef=\"/nop/schema/xdef.xdef\"", // + "xmlns:xdef=\"/nop/schema/xdef.xdef\""), // + "/nop/schema/xdef.xdef" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xmlns:x=\"x\"", // + "xmlns:x=\"x\""), // + null // + ); // - 在 *.xdef 中引用内部名字 - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:ref=\"XDefNode\"", - "meta:ref=\"XDefNode\""), - "meta:define#meta:name=XDefNode"); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:ref=\"DslNode\"", - "xdef:ref=\"DslNode\""), - "xdef:unknown-tag#xdef:name=DslNode"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:ref=\"XDefNode\"", // + "meta:ref=\"XDefNode\""), // + "meta:define#meta:name=XDefNode" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdef:ref=\"DslNode\"", // + "xdef:ref=\"DslNode\""), // + "xdef:unknown-tag#xdef:name=DslNode" // + ); // // - 引用文件的相对路径出现在开头:单元测试中暂时无法查找 vfs 相对路径 -// assertReference(readVfsResource("/nop/schema/xui/simple-component.xdef").replace( -// "xdef:ref=\"../xui/import.xdef\"", -// "xdef:ref=\"../xui/import.xdef\""), "/nop/schema/xui/import.xdef"); +// assertReference(insertCaretIntoVfs("/nop/schema/xui/simple-component.xdef", // +// "xdef:ref=\"../xui/import.xdef\"", // +// "xdef:ref=\"../xui/import.xdef\""), // +// "/nop/schema/xui/import.xdef" // +// ); assertReference(""" - """, "xdef:define#xdef:name=PropNode"); + """, // + "xdef:define#xdef:name=PropNode" // + ); assertReference(""" - """, null); + """, // + null // + ); // - 在 *.xdef 中引用外部文件 assertReference(""" - """, "/nop/schema/xdsl.xdef"); + """, // + "/nop/schema/xdsl.xdef" // + ); assertReference(""" - """, "/nop/schema/schema/obj-schema.xdef"); + """, // + "/nop/schema/schema/obj-schema.xdef" // + ); // - 在 *.xmeta 中引用外部文件中的节点 assertReference(""" - """, "/test/reference/test-filter.xdef?xdef:define#xdef:name=FilterCondition"); + """, // + "/test/reference/test-filter.xdef?xdef:define#xdef:name=FilterCondition" // + ); // - 外部文件中的引用节点不存在 assertReference(""" - """, null); + """, // + null // + ); // 对 x:prototype 属性值的引用 - assertReference(readVfsResource("/test/reference/user.view.xml").replace("x:prototype=\"list\"", - "x:prototype=\"list\""), - "grid#id=list"); - assertReference(readVfsResource("/test/reference/a.xlib").replace("x:prototype=\"Get\"", - "x:prototype=\"Get\""), "Get"); + assertReference(insertCaretIntoVfs("/test/reference/user.view.xml", // + "x:prototype=\"list\"", // + "x:prototype=\"list\""), // + "grid#id=list" // + ); + assertReference(insertCaretIntoVfs("/test/reference/a.xlib", // + "x:prototype=\"Get\"", // + "x:prototype=\"Get\""), // + "Get" // + ); // - 引用不存在 assertReference(""" @@ -546,25 +732,35 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { - """, null); + """, // + null // + ); assertReference(""" - """, null); + """, // + null // + ); // 对唯一键的引用 - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"name\"", - "meta:unique-attr=\"name\""), - "xdef:prop#name=!xml-name"); - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace("meta:unique-attr=\"xdef:name\"", - "meta:unique-attr=\"xdef:name\""), - "xdef:define#xdef:name=!var-name"); - assertReference(readVfsResource("/nop/schema/xmeta.xdef").replace("xdef:key-attr=\"id\"", - "xdef:key-attr=\"id\""), - "selection#id=!var-name"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:unique-attr=\"name\"", // + "meta:unique-attr=\"name\""), // + "xdef:prop#name=!xml-name" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "meta:unique-attr=\"xdef:name\"", // + "meta:unique-attr=\"xdef:name\""), // + "xdef:define#xdef:name=!var-name" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xmeta.xdef", // + "xdef:key-attr=\"id\"", // + "xdef:key-attr=\"id\""), // + "selection#id=!var-name" // + ); // - 引用不存在 assertReference(""" - """, null); + """, // + null // + ); assertReference(""" - """, null); + """, // + null // + ); assertReference(""" - """, "/nop/core/xlib/meta-gen.xlib"); + """, // + "/nop/core/xlib/meta-gen.xlib" // + ); assertReference(""" @@ -602,15 +804,21 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { - """, "/nop/core/xlib/meta-gen.xlib"); + """, // + "/nop/core/xlib/meta-gen.xlib" // + ); // 非有效路径或未定义属性引用 - assertReference(readVfsResource("/test/reference/user.view.xml").replace("xmlns:view-gen=\"view-gen\"", - "xmlns:view-gen=\"view-gen\""), - null); + assertReference(insertCaretIntoVfs("/test/reference/user.view.xml", // + "xmlns:view-gen=\"view-gen\"", // + "xmlns:view-gen=\"view-gen\""), // + null // + ); assertReference(""" - """, null); + """, // + null // + ); // generic-type、class-name、package-name 类型的值引用 assertReference(""" @@ -619,43 +827,59 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { > - """, "java.lang.String"); + """, // + "java.lang.String" // + ); assertReference(""" - """, "io.nop.xui.initialize.VueNodeStdDomainHandler"); + """, // + "io.nop.xui.initialize.VueNodeStdDomainHandler" // + ); assertReference(""" - """, "io.nop.xlang.xdef"); + """, // + "io.nop.xlang.xdef" // + ); assertReference(""" - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler" // + ); assertReference(""" - """, null); + """, // + null // + ); // dict/enum 类型的值引用 - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace("xdef:default-override=\"append\"", - "xdef:default-override=\"append\""), // - "io.nop.xlang.xdef.XDefOverride#APPEND"); - assertReference(readVfsResource("/nop/schema/xlib.xdef").replace("macro=\"!boolean=false\"", - "macro=\"!boolean=false\""), // - "/dict/core/std-domain.dict.yaml#boolean"); - assertReference(readVfsResource("/nop/schema/xlib.xdef").replace("macro=\"!boolean=false\"", - "macro=\"!boolean=false\""), // - null); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdef:default-override=\"append\"", // + "xdef:default-override=\"append\""), // + "io.nop.xlang.xdef.XDefOverride#APPEND" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xlib.xdef", // + "macro=\"!boolean=false\"", // + "macro=\"!boolean=false\""), // + "/dict/core/std-domain.dict.yaml#boolean" // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xlib.xdef", // + "macro=\"!boolean=false\"", // + "macro=\"!boolean=false\""), // + null // + ); assertReference(""" - """, "/dict/test/doc/child-type.dict.yaml#leaf"); + """, // + "/dict/test/doc/child-type.dict.yaml#leaf" // + ); // x:schema 指定的 *.xdef 不存在,使得 DSL 的元模型未定义,导致模型属性未知,其引用将无法识别 // - *.xdef 不存在 assertReference(""" - """, null); + """, // + null // + ); // - 属性未定义,引用无法识别 assertReference(""" - """, null); + """, // + null // + ); // // TODO 对 xpl 属性的文件引用 // assertReference(""" // -// """, "/test/reference/a.xlib"); +// """, // +// "/test/reference/a.xlib" // +// ); // assertReference(""" // -// """, "/test/reference/a.xlib"); +// """, // +// "/test/reference/a.xlib" // +// ); } public void testAttributeValueDefTypeReferences() { - assertReference(readVfsResource("/nop/schema/xdef.xdef").replace( - "xdef:default-override=\"enum:io.nop.xlang.xdef.XDefOverride\"", - "xdef:default-override=\"enum:io.nop.xlang.xdef.XDefOverride\""), - "io.nop.xlang.xdef.XDefOverride"); + assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "xdef:default-override=\"enum:io.nop.xlang.xdef.XDefOverride\"", // + "xdef:default-override=\"enum:io.nop.xlang.xdef.XDefOverride\""), // + "io.nop.xlang.xdef.XDefOverride" // + ); // 引用字典中定义的数据域 assertReference(""" @@ -699,14 +934,18 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { > - """, "/dict/core/std-domain.dict.yaml#v-path"); + """, // + "/dict/core/std-domain.dict.yaml#v-path" // + ); assertReference(""" - """, "/dict/core/std-domain.dict.yaml#string"); + """, // + "/dict/core/std-domain.dict.yaml#string" // + ); // 字典/枚举的 options 引用 assertReference(""" @@ -715,18 +954,23 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { > - """, "/dict/test/doc/child-type.dict.yaml"); + """, // + "/dict/test/doc/child-type.dict.yaml" // + ); assertReference(""" - """, null); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace( - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // - "io.nop.xlang.xdef.XDefOverride"); + """, // + null // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", // + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + "io.nop.xlang.xdef.XDefOverride" // + ); // 字典/枚举的默认值引用 assertReference(""" @@ -735,18 +979,23 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { > - """, "/dict/test/doc/child-type.dict.yaml#leaf"); + """, // + "/dict/test/doc/child-type.dict.yaml#leaf" // + ); assertReference(""" - """, null); - assertReference(readVfsResource("/nop/schema/xdsl.xdef").replace( - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", - "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // - "io.nop.xlang.xdef.XDefOverride#MERGE"); + """, // + null // + ); + assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\"", // + "x:override=\"enum:io.nop.xlang.xdef.XDefOverride=merge\""), // + "io.nop.xlang.xdef.XDefOverride#MERGE" // + ); // 缺省属性值中 @attr: 引用 assertReference(""" @@ -755,26 +1004,35 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { > - """, "import#name=var-name"); + """, // + "import#name=var-name" // + ); assertReference(""" - """, "var#type=!string"); + """, // + "var#type=!string" // + ); assertReference(""" - """, null); + """, // + null // + ); } public void testTextReferences() { - assertReference(readVfsResource("/test/reference/user.view.xml").replace("", ""), - "/test/reference/a.xmeta"); + assertReference(insertCaretIntoVfs("/test/reference/user.view.xml", // + "", // + ""), // + "/test/reference/a.xmeta" // + ); assertReference(""" /test/reference/test-filter.xdef,/nop/schema/xdsl.xdef - """, "/test/reference/test-filter.xdef"); + """, // + "/test/reference/test-filter.xdef" // + ); assertReference(""" dsl.xdef
- """, "/nop/schema/xdsl.xdef"); + """, // + "/nop/schema/xdsl.xdef" // + ); assertReference(""" t-filter.xdef, /nop/schema/xdsl.xdef ]]> - """, "/test/reference/test-filter.xdef"); + """, // + "/test/reference/test-filter.xdef" // + ); assertReference(""" /xdsl.xdef ]]> - """, "/nop/schema/xdsl.xdef"); + """, // + "/nop/schema/xdsl.xdef" // + ); } /** 通过在 text 中插入 <caret> 代表光标位置 */ diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java index ebebc4235..b4796d6fb 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java @@ -47,55 +47,69 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase { assertCompletion(""" let s = "abc"; s.toCharA - """, """ + """, // + """ let s = "abc"; s.toCharArray - """); + """ // + ); assertCompletion(""" const handler = new io.nop.xlang.xdef.domain.XJsonDomai - """, """ + """, // + """ const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler - """); + """ // + ); assertCompletion(""" const handler = new io.nop.xlang.xdef.d - """, """ + """, // + """ const handler = new io.nop.xlang.xdef.domain - """); + """ // + ); assertCompletion(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = XJsonDomainHandler.INST - """, """ + """, // + """ import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = XJsonDomainHandler.INSTANCE - """); + """ // + ); assertCompletion(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler(); handler.instan - """, """ + """, // + """ import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler(); handler.instance - """); + """ // + ); assertCompletion(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler.Su - """, """ + """, // + """ import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler.Sub - """); + """ // + ); assertCompletion(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler.Sub(); handler.ag - """, """ + """, // + """ import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler.Sub(); handler.age - """); + """ // + ); } public void testCompletionInXLib() { @@ -114,7 +128,8 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase { - """, """ + """, // + """ @@ -129,7 +144,8 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase { - """); + """ // + ); assertCompletionInXLib(""" @@ -143,7 +159,8 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase { - """, """ + """, // + """ @@ -156,7 +173,8 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase { - """); + """ // + ); } /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */ diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index 836f0fb6d..e8c4e25b1 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -16,7 +16,9 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { import java.lang.; const abc = ; const abc = () =>; - """, "/test/ast/xlang-script-err-1.ast"); + """, // + "/test/ast/xlang-script-err-1.ast" // + ); assertASTTree(""" import java.lang.String; @@ -59,7 +61,8 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { a.b(b, 1); } """, // - "/test/ast/xlang-script-1.ast"); + "/test/ast/xlang-script-1.ast" // + ); } protected void assertASTTree(String code, String expectedAstFile) { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java index d26118ded..8c878e47d 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -42,28 +42,40 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { let a = b; "abc".trim(); let a = b; - """, "java.lang.String#trim"); + """, // + "java.lang.String#trim" // + ); assertReference(""" let abc = 123; abc.byteValue(); let a = b; - """, "java.lang.Integer#byteValue"); + """, // + "java.lang.Integer#byteValue" // + ); assertReference(""" let abc = 123; abc = "abc"; abc.trim(); let a = b; - """, "java.lang.String#trim"); + """, // + "java.lang.String#trim" // + ); assertReference(""" Integer.valueOf; - """, "java.lang.Integer#valueOf"); + """, // + "java.lang.Integer#valueOf" // + ); assertReference(""" Integer.valueOf(); - """, "java.lang.Integer#valueOf"); + """, // + "java.lang.Integer#valueOf" // + ); assertReference(""" Integer.valueOf('123'); - """, "java.lang.Integer#valueOf"); + """, // + "java.lang.Integer#valueOf" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; @@ -71,27 +83,35 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { handler.getName(); // 尝试触发无限递归 let name = handler.getName(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler(); handler.instance().getName(); // 尝试触发无限递归 let name = handler.getName(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = XJsonDomainHandler.INSTANCE; // 尝试触发无限递归 let name = handler.getName(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#INSTANCE"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler#INSTANCE" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; let name = XJsonDomainHandler.INSTANCE.getName(); // 尝试触发无限递归 const handler = new XJsonDomainHandler(); name = handler.getName(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName" // + ); } public void testVarReference() { @@ -99,174 +119,249 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { let abc = "abc"; const def = abc + "def"; abc = 123; - """, "@4"); + """, // + "@4" // + ); assertReference(""" let abc = "abc"; abc.trim(); - """, "@4"); + """, // + "@4" // + ); assertReference(""" const def = [1, 2, 3]; def[0] = 2; - """, "@6"); + """, // + "@6" // + ); assertReference(""" - const s = new String("abc"); - """, "java.lang.String"); + const s = new String("abc" // + ); + """, // + "java.lang.String" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const handler = new XJsonDomainHandler(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const sub = new XJsonDomainHandler.Sub.Sub(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const sub = new XJsonDomainHandler.Sub.Sub(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub" // + ); assertReference(""" const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler(); - """, "io.nop.xlang.xdef"); + """, // + "io.nop.xlang.xdef" // + ); assertReference(""" const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler" // + ); assertReference(""" const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler" // + ); assertReference(""" const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub" // + ); assertReference(""" const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub" // + ); assertReference(""" const handler = new io.nop.xlang.xdef.domain.XJsonDomainHandler(); handler.getName(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler#getName" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; const sub = new XJsonDomainHandler.Sub.Sub(); sub.getName(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName" // + ); assertReference(""" const sub = new io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub(); sub.getName(); - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName" // + ); assertReference(""" const a1 = 'a'; const b1 = 1; const obj = {a1, b1: b1}; - """, "@6"); + """, // + "@6" // + ); assertReference(""" const a1 = 'a'; const b1 = 1; const obj = {a1, b1: b1}; - """, "@22"); + """, // + "@22" // + ); assertReference(""" const a1 = 'a'; const b1 = 1; const c1 = 'c'; const obj = {a1, c1}; - """, "@36"); + """, // + "@36" // + ); } public void testFunctionReference() { assertReference(""" function fn1(a, b) { return a + b; } fn1(1, 2); - """, "@9"); + """, // + "@9" // + ); assertReference(""" function fn1(a, b) { return a + b; } const a = fn1(1, 2); - """, "@9"); + """, // + "@9" // + ); assertReference(""" const fn1 = (a1, b1) => a1 + b1; fn1(1, 2); - """, "@6"); + """, // + "@6" // + ); assertReference(""" const fn1 = (a1, b1) => a1 + b1; const a = fn1(1, 2); - """, "@6"); + """, // + "@6" // + ); // 对函数参数的引用 assertReference(""" function fn1(a1, b1) { return a1 + b1; } - """, "@13"); + """, // + "@13" // + ); assertReference(""" function fn1(a1, b1) { return a1 + b1; } - """, "@17"); + """, // + "@17" // + ); assertReference(""" const fn1 = (a1, b1) => a1 + b1; - """, "@13"); + """, // + "@13" // + ); assertReference(""" const fn1 = (a1, b1) => a1 + b1; - """, "@17"); + """, // + "@17" // + ); // 对函数参数类型的引用 assertReference(""" function fn1(a1: string, b1: number) { return a1 + b1; } - """, "java.lang.String"); + """, // + "java.lang.String" // + ); assertReference(""" function fn1(a1: string, b1: number) { return a1 + b1; } - """, "java.lang.Number"); + """, // + "java.lang.Number" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; function fn1(a1: string, b1: XJsonDomainHandler) { return a1 + b1; } - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; function fn1(a1: string, b1: XJsonDomainHandler.Sub.Sub) { return a1 + b1; } - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub" // + ); assertReference(""" import io.nop.xlang.xdef.domain.XJsonDomainHandler; function fn1(a1: XJsonDomainHandler.Sub.Sub) { return a1.getName(); } - """, "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName"); + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler.Sub.Sub#getName" // + ); assertReference(""" const fn1 = (a1: string, b1: number) => a1 + b1; - """, "java.lang.String"); + """, // + "java.lang.String" // + ); assertReference(""" const fn1 = (a1: string, b1: number) => a1 + b1; - """, "java.lang.Number"); + """, // + "java.lang.Number" // + ); assertReference(""" const b = s instanceof string; - """, "java.lang.String"); + """, // + "java.lang.String" // + ); assertReference(""" const b = s instanceof number; - """, "java.lang.Number"); + """, // + "java.lang.Number" // + ); // // TODO 对函数调用的结果类型的引用 // assertReference(""" // function fn1(a, b) { return a + b; } // const a = fn1('a', 'b'); // a.trim(); -// """, "java.lang.String#trim"); +// """, // +// "java.lang.String#trim" // +// ); // assertReference(""" // function fn1(a, b) { return a + b; } // const a = fn1('a', 'b'); // a.trim(); // const b = fn1(1, 2); // b.byteValue(); -// """, "java.lang.Integer#byteValue"); +// """, // +// "java.lang.Integer#byteValue" // +// ); } public void testGenericTypeReference() { @@ -274,17 +369,23 @@ public class TestXLangScriptReferences extends BaseXLangPluginTestCase { import java.util.HashMap; import java.util.List; const s = new HashMap(); - """, "java.util.HashMap"); + """, // + "java.util.HashMap" // + ); assertReference(""" import java.util.HashMap; import java.util.List; const s = new HashMapring, List>(); - """, "java.lang.String"); + """, // + "java.lang.String" // + ); assertReference(""" import java.util.HashMap; import java.util.List; const s = new HashMapst>(); - """, "java.util.List"); + """, // + "java.util.List" // + ); } /** 通过在 text 中插入 <caret> 代表光标位置 */ diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java index 40db9db12..02207b7d9 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java @@ -19,304 +19,414 @@ public class TestXLangScriptRename extends BaseXLangPluginTestCase { private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); public void testRenameVar() { - assertRename("data", """ - let abc = 123; - const def = abc + 1; - let s = abc + ' abc'; - """, """ + assertRename("data", // + """ + let abc = 123; + const def = abc + 1; + let s = abc + ' abc'; + """, // + """ let data = 123; const def = data + 1; let s = data + ' abc'; - """); - assertRename("data", """ - let abc = 123; - const def = abc + 1; - let s = abc + ' abc'; - """, """ + """ // + ); + assertRename("data", // + """ + let abc = 123; + const def = abc + 1; + let s = abc + ' abc'; + """, // + """ let data = 123; const def = data + 1; let s = data + ' abc'; - """); + """ // + ); - assertRename("data", """ - let abc = 'abc'; - abc.trim(); - abc.isEmpty(); - """, """ + assertRename("data", // + """ + let abc = 'abc'; + abc.trim(); + abc.isEmpty(); + """, // + """ let data = 'abc'; data.trim(); data.isEmpty(); - """); - assertRename("data", """ - let abc = 'abc'; - abc.trim(); - abc.isEmpty(); - """, """ + """ // + ); + assertRename("data", // + """ + let abc = 'abc'; + abc.trim(); + abc.isEmpty(); + """, // + """ let data = 'abc'; data.trim(); data.isEmpty(); - """); + """ // + ); - assertRename("data", """ - let abc = 'abc'; - let def = {abc, def: 123}; - """, """ + assertRename("data", // + """ + let abc = 'abc'; + let def = {abc, def: 123}; + """, // + """ let data = 'abc'; let def = {data, def: 123}; - """); - assertRename("data", """ - let abc = 'abc'; - let def = {abc, def: 123}; - """, """ + """ // + ); + assertRename("data", // + """ + let abc = 'abc'; + let def = {abc, def: 123}; + """, // + """ let data = 'abc'; let def = {data, def: 123}; - """); - assertRename("data", """ - let abc = 'abc'; - let def = {abc: abc, def: 123}; - """, """ + """ // + ); + assertRename("data", // + """ + let abc = 'abc'; + let def = {abc: abc, def: 123}; + """, // + """ let data = 'abc'; let def = {abc: data, def: 123}; - """); - assertRename("data", """ - let abc = 'abc'; - let def = {abc: abc, def: 123}; - """, """ + """ // + ); + assertRename("data", // + """ + let abc = 'abc'; + let def = {abc: abc, def: 123}; + """, // + """ let data = 'abc'; let def = {abc: data, def: 123}; - """); + """ // + ); } public void testRenameFunction() { - assertRename("fn_1", """ - function fn(a, b) {} - const r = fn(1, 2); - """, """ + assertRename("fn_1", // + """ + function fn(a, b) {} + const r = fn(1, 2); + """, // + """ function fn_1(a, b) {} const r = fn_1(1, 2); - """); - assertRename("fn_1", """ - function fn(a, b) {} - const r = fn(1, 2); - """, """ + """ // + ); + assertRename("fn_1", // + """ + function fn(a, b) {} + const r = fn(1, 2); + """, // + """ function fn_1(a, b) {} const r = fn_1(1, 2); - """); + """ // + ); - assertRename("fn_1", """ - const fn = (a, b) => {}; - const r = fn(1, 2); - """, """ + assertRename("fn_1", // + """ + const fn = (a, b) => {}; + const r = fn(1, 2); + """, // + """ const fn_1 = (a, b) => {}; const r = fn_1(1, 2); - """); - assertRename("fn_1", """ - const fn = (a, b) => {}; - const r = fn(1, 2); - """, """ + """ // + ); + assertRename("fn_1", // + """ + const fn = (a, b) => {}; + const r = fn(1, 2); + """, // + """ const fn_1 = (a, b) => {}; const r = fn_1(1, 2); - """); + """ // + ); - assertRename("aa", """ - function fn(a1, b1) { return a1 + b1; } - """, """ + assertRename("aa", // + """ + function fn(a1, b1) { return a1 + b1; } + """, // + """ function fn(aa, b1) { return aa + b1; } - """); - assertRename("aa", """ - function fn(a1, b1) { return a1 + b1; } - """, """ + """ // + ); + assertRename("aa", // + """ + function fn(a1, b1) { return a1 + b1; } + """, // + """ function fn(aa, b1) { return aa + b1; } - """); - assertRename("bb", """ - function fn(a1, b1) { return a1 + b1; } - """, """ + """ // + ); + assertRename("bb", // + """ + function fn(a1, b1) { return a1 + b1; } + """, // + """ function fn(a1, bb) { return a1 + bb; } - """); - assertRename("bb", """ - function fn(a1, b1) { return a1 + b1; } - """, """ + """ // + ); + assertRename("bb", // + """ + function fn(a1, b1) { return a1 + b1; } + """, // + """ function fn(a1, bb) { return a1 + bb; } - """); + """ // + ); - assertRename("aa", """ - const fn = (a1, b1) => a1 + b1; - """, """ + assertRename("aa", // + """ + const fn = (a1, b1) => a1 + b1; + """, // + """ const fn = (aa, b1) => aa + b1; - """); - assertRename("aa", """ - const fn = (a1, b1) => a1 + b1; - """, """ + """ // + ); + assertRename("aa", // + """ + const fn = (a1, b1) => a1 + b1; + """, // + """ const fn = (aa, b1) => aa + b1; - """); - assertRename("bb", """ - const fn = (a1, b1) => a1 + b1; - """, """ + """ // + ); + assertRename("bb", // + """ + const fn = (a1, b1) => a1 + b1; + """, // + """ const fn = (a1, bb) => a1 + bb; - """); - assertRename("bb", """ - const fn = (a1, b1) => a1 + b1; - """, """ + """ // + ); + assertRename("bb", // + """ + const fn = (a1, b1) => a1 + b1; + """, // + """ const fn = (a1, bb) => a1 + bb; - """); + """ // + ); - assertRename("aa", """ - function fn(a1, b1) { - const c = a1 + 1; - return c + a1 + b1; - } - """, """ + assertRename("aa", // + """ + function fn(a1, b1) { + const c = a1 + 1; + return c + a1 + b1; + } + """, // + """ function fn(aa, b1) { const c = aa + 1; return c + aa + b1; } - """); - assertRename("aa", """ - function fn(a1, b1) { - const c = a1 + 1; - return c + a1 + b1; - } - """, """ + """ // + ); + assertRename("aa", // + """ + function fn(a1, b1) { + const c = a1 + 1; + return c + a1 + b1; + } + """, // + """ function fn(aa, b1) { const c = aa + 1; return c + aa + b1; } - """); - assertRename("aa", """ - function fn(a1, b1) { - const c = a1 + 1; - return c + a1 + b1; - } - """, """ + """ // + ); + assertRename("aa", // + """ + function fn(a1, b1) { + const c = a1 + 1; + return c + a1 + b1; + } + """, // + """ function fn(aa, b1) { const c = aa + 1; return c + aa + b1; } - """); + """ // + ); - assertRename("aa", """ - const fn = (a1, b1) => { - const c = a1 + 1; - return c + a1 + b1; - }; - """, """ + assertRename("aa", // + """ + const fn = (a1, b1) => { + const c = a1 + 1; + return c + a1 + b1; + }; + """, // + """ const fn = (aa, b1) => { const c = aa + 1; return c + aa + b1; }; - """); - assertRename("aa", """ - const fn = (a1, b1) => { - const c = a1 + 1; - return c + a1 + b1; - }; - """, """ + """ // + ); + assertRename("aa", // + """ + const fn = (a1, b1) => { + const c = a1 + 1; + return c + a1 + b1; + }; + """, // + """ const fn = (aa, b1) => { const c = aa + 1; return c + aa + b1; }; - """); - assertRename("aa", """ - const fn = (a1, b1) => { - const c = a1 + 1; - return c + a1 + b1; - }; - """, """ + """ // + ); + assertRename("aa", // + """ + const fn = (a1, b1) => { + const c = a1 + 1; + return c + a1 + b1; + }; + """, // + """ const fn = (aa, b1) => { const c = aa + 1; return c + aa + b1; }; - """); + """ // + ); } public void testRenameJava() { - assertJavaRename("name", """ - package io.nop.xlang.xdef; - public class Sample { - public void getName() {} - } - """, """ + assertJavaRename("name", // + """ + package io.nop.xlang.xdef; + public class Sample { + public void getName() {} + } + """, // + """ import io.nop.xlang.xdef.Sample; let s = new Sample(); s.getName(); - """, """ + """, // + """ import io.nop.xlang.xdef.Sample; let s = new Sample(); s.name(); - """); - assertJavaRename("name", """ - package io.nop.xlang.xdef; - public class Sample { - public void getName() {} - } - """, """ + """ // + ); + assertJavaRename("name", // + """ + package io.nop.xlang.xdef; + public class Sample { + public void getName() {} + } + """, // + """ let s = new io.nop.xlang.xdef.Sample(); s.getName(); - """, """ + """, // + """ let s = new io.nop.xlang.xdef.Sample(); s.name(); - """); + """ // + ); - assertJavaRename("Sample123", """ - package io.nop.xlang.xdef; - public class Sample { } - """, """ + assertJavaRename("Sample123", // + """ + package io.nop.xlang.xdef; + public class Sample { } + """, // + """ import io.nop.xlang.xdef.Sample; let s = new Sample(); - """, """ + """, // + """ import io.nop.xlang.xdef.Sample123; let s = new Sample123(); - """); - assertJavaRename("Sample456", """ - package io.nop.xlang.xdef; - public class Sample { } - """, """ + """ // + ); + assertJavaRename("Sample456", // + """ + package io.nop.xlang.xdef; + public class Sample { } + """, // + """ let s = new io.nop.xlang.xdef.Sample(); - """, """ + """, // + """ let s = new io.nop.xlang.xdef.Sample456(); - """); - assertJavaRename("Sample789", """ - package io.nop.xlang.xdef; - public class Sample { } - """, """ + """ // + ); + assertJavaRename("Sample789", // + """ + package io.nop.xlang.xdef; + public class Sample { } + """, // + """ let s = new io.nop.xlang.xdef. Sample(); - """, """ + """, // + """ let s = new io.nop.xlang.xdef. Sample789(); - """); + """ // + ); - assertJavaRename("username", """ - package io.nop.xlang.xdef; - public class Sample { public final String name; } - """, """ + assertJavaRename("username", // + """ + package io.nop.xlang.xdef; + public class Sample { public final String name; } + """, // + """ import io.nop.xlang.xdef.Sample; let s = new Sample(); let name = s.name; - """, """ + """, // + """ import io.nop.xlang.xdef.Sample; let s = new Sample(); let name = s.username; - """); - assertJavaRename("username", """ - package io.nop.xlang.xdef; - public class Sample { public final String name; } - """, """ + """ // + ); + assertJavaRename("username", // + """ + package io.nop.xlang.xdef; + public class Sample { public final String name; } + """, // + """ let s = new io.nop.xlang.xdef.Sample(); let name = s.name; - """, """ + """, // + """ let s = new io.nop.xlang.xdef.Sample(); let name = s.username; - """); + """ // + ); - assertJavaRename("xpl", """ - package io.nop.xlang.xdef; - public class Sample { } - """, """ + assertJavaRename("xpl", // + """ + package io.nop.xlang.xdef; + public class Sample { } + """, // + """ import io.nop.xlang.xdef.Sample; let s = new io.nop.xlang.xdef.Sample(); - """, """ + """, // + """ import io.nop.xpl.xdef.Sample; let s = new io.nop.xpl.xdef.Sample(); - """); + """ // + ); } protected void assertRename(String newName, String text, String expectedText) { -- Gitee From c76f0f2920a8d8ed80ea21f6309413b15765529f Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Sun, 20 Jul 2025 19:59:41 +0800 Subject: [PATCH 69/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B=20XL?= =?UTF-8?q?ang=20=E8=8A=82=E7=82=B9=E5=92=8C=E5=B1=9E=E6=80=A7=E5=90=8D?= =?UTF-8?q?=E5=AD=97=E7=9A=84=E8=87=AA=E5=8A=A8=E8=A1=A5=E5=85=A8=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../idea/plugin/annotator/XLangAnnotator.java | 10 +- .../XLangCompletionContributor.java | 182 ++++---- .../doc/XLangDocumentationProvider.java | 10 +- .../idea/plugin/lang/XLangDocumentation.java | 4 +- .../idea/plugin/lang/psi/XLangAttribute.java | 12 +- .../plugin/lang/psi/XLangAttributeValue.java | 6 +- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 10 +- .../reference/XLangAttributeReference.java | 173 ++++++++ .../lang/reference/XLangDefAttrReference.java | 66 --- .../lang/reference/XLangTagReference.java | 111 ++++- .../nop/idea/plugin/utils/XDefPsiHelper.java | 119 ----- .../nop/idea/plugin/utils/XmlPsiHelper.java | 69 +-- .../io/nop/idea/plugin/utils/XmlTagInfo.java | 279 ------------ .../idea/plugin/BaseXLangPluginTestCase.java | 32 +- .../TestXLangCompletionContributor.java | 36 -- .../plugin/lang/TestXLangCompletions.java | 410 ++++++++++++++++++ .../nop/idea/plugin/lang/TestXLangParser.java | 2 +- .../lang/TestXLangScriptCompletions.java | 2 +- .../plugin/lang/TestXLangScriptParser.java | 2 +- .../test/resources/_vfs/test/doc/example.xdef | 6 +- 20 files changed, 879 insertions(+), 662 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java delete mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java index 3eb2c5fdc..2f17db54f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java @@ -215,8 +215,8 @@ public class XLangAnnotator implements Annotator { private void checkAttrValue(@NotNull AnnotationHolder holder, @NotNull XLangAttributeValue attrValue) { XLangAttribute attr = attrValue.getParentAttr(); - IXDefAttribute attrDef = attr != null ? attr.getDefAttr() : null; - if (attrDef == null) { + IXDefAttribute defAttr = attr != null ? attr.getDefAttr() : null; + if (defAttr == null) { return; } @@ -224,9 +224,9 @@ public class XLangAnnotator implements Annotator { String attrValueText = attrValue.getValue(); TextRange attrValueTextRange = attrValue.getValueTextRange(); - XDefTypeDecl attrType = attrDef.getType(); + XDefTypeDecl defAttrType = defAttr.getType(); if (StringHelper.isEmpty(attrValueText)) { - if (attrType.isMandatory()) { + if (defAttrType.isMandatory()) { errorAnnotation(holder, attrValue.getTextRange(), "xlang.annotation.attr.value-required", attrName); } return; @@ -234,7 +234,7 @@ public class XLangAnnotator implements Annotator { // Note: dict/enum 的有效值检查由 PsiReference 处理 SourceLocation loc = XmlPsiHelper.getLocation(attrValue); - checkStdDomain(holder, attrValueTextRange, attrType.getStdDomain(), loc, attrName, attrValueText); + checkStdDomain(holder, attrValueTextRange, defAttrType.getStdDomain(), loc, attrName, attrValueText); } private void checkStdDomain( diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java index 9f080bb46..47bf0800b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java @@ -7,6 +7,9 @@ */ package io.nop.idea.plugin.completion; +import java.util.Objects; +import java.util.Set; + import com.intellij.codeInsight.completion.CompletionContributor; import com.intellij.codeInsight.completion.CompletionInitializationContext; import com.intellij.codeInsight.completion.CompletionParameters; @@ -25,34 +28,31 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlElementType; -import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTokenType; import com.intellij.util.PlatformIcons; import io.nop.api.core.beans.DictBean; import io.nop.api.core.beans.DictOptionBean; import io.nop.api.core.convert.ConvertHelper; import io.nop.core.dict.DictProvider; +import io.nop.idea.plugin.lang.psi.XLangAttribute; +import io.nop.idea.plugin.lang.psi.XLangAttributeValue; +import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.resource.ProjectEnv; -import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; -import io.nop.idea.plugin.utils.XmlTagInfo; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.IXDefComment; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.XDefConstants; import io.nop.xlang.xdef.XDefTypeDecl; +import io.nop.xlang.xdsl.XDslKeys; import org.jetbrains.annotations.NotNull; -import java.util.Objects; -import java.util.Set; - /** * Completion相关的文档可以参见{@link CompletionContributor}类的注释 * 或者 https://www.plugin-dev.com/intellij/custom-language/code-completion/ */ public class XLangCompletionContributor extends CompletionContributor implements DumbAware { - public XLangCompletionContributor() { } @@ -61,19 +61,28 @@ public class XLangCompletionContributor extends CompletionContributor implements public void beforeCompletion(@NotNull final CompletionInitializationContext context) { final int offset = context.getStartOffset(); final PsiFile file = context.getFile(); - final XmlAttributeValue attributeValue = PsiTreeUtil.findElementOfClassAtOffset(file, offset, XmlAttributeValue.class, true); + final XmlAttributeValue attributeValue = PsiTreeUtil.findElementOfClassAtOffset(file, + offset, + XmlAttributeValue.class, + true); if (attributeValue != null && offset == attributeValue.getTextRange().getStartOffset()) { context.setDummyIdentifier(""); } final PsiElement at = file.findElementAt(offset); - if (at != null && at.getNode().getElementType() == XmlTokenType.XML_NAME && at.getParent() instanceof XmlAttribute) { - context.getOffsetMap().addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, at.getTextRange().getEndOffset()); + if (at != null + && at.getNode().getElementType() == XmlTokenType.XML_NAME + && at.getParent() instanceof XmlAttribute // + ) { + context.getOffsetMap() + .addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, at.getTextRange().getEndOffset()); } + if (at != null && at.getParent() instanceof XmlAttributeValue) { final int end = at.getParent().getTextRange().getEndOffset(); final Document document = context.getEditor().getDocument(); final int lineEnd = document.getLineEndOffset(document.getLineNumber(offset)); + if (lineEnd < end) { context.setReplacementOffset(lineEnd); } @@ -82,11 +91,14 @@ public class XLangCompletionContributor extends CompletionContributor implements // 情况比较简单,因此没有使用extend来注册CompletionProvider,而是直接实现此方法 @Override - public void fillCompletionVariants(@NotNull final CompletionParameters parameters, @NotNull CompletionResultSet result) { + public void fillCompletionVariants( + @NotNull final CompletionParameters parameters, @NotNull CompletionResultSet result + ) { PsiElement element = parameters.getPosition().getParent(); ASTNode node = element.getNode(); - if (node == null) + if (node == null) { return; + } // return early when there's not prefix // String prefix = result.getPrefixMatcher().getPrefix(); @@ -102,93 +114,113 @@ public class XLangCompletionContributor extends CompletionContributor implements private void doFillCompletion(PsiElement element, @NotNull CompletionResultSet result) { IElementType elType = element.getNode().getElementType(); + if (elType == XmlElementType.XML_TAG) { - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(element.getParent()); - if (tagInfo == null || tagInfo.getDefNode() == null) - return; + XLangTag tag = (XLangTag) element; - XmlTag parent = (XmlTag) element.getParent(); - Set childTagNames = XmlPsiHelper.getChildTagNames(parent); + XLangTag parentTag = tag.getParentTag(); + IXDefNode parentTagDefNode = parentTag != null ? parentTag.getSchemaDefNode() : null; + if (parentTag == null) { + return; + } - for (IXDefNode childNode : tagInfo.getDefNode().getChildren().values()) { - if (childNode.isInternal()) + Set existChildTagNames = XmlPsiHelper.getChildTagNames(parentTag); + for (IXDefNode childNode : parentTagDefNode.getChildren().values()) { + if (childNode.isInternal()) { continue; + } - if (childNode.isAllowMultiple() || !childTagNames.contains(childNode.getTagName())) { + if (childNode.isAllowMultiple() || !existChildTagNames.contains(childNode.getTagName())) { result.addElement(buildTag(childNode.getTagName(), childNode)); } } + result.stopHere(); - } else if (elType == XmlElementType.XML_ATTRIBUTE) { - XmlAttribute attr = (XmlAttribute) element; - XmlTag tag = XmlPsiHelper.getXmlTag(attr); - if (tag != null) { - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(tag); - if (tagInfo == null) - return; - - String prefix = result.getPrefixMatcher().getPrefix(); - IXDefNode defNode = tagInfo.getDefNode(); - if (prefix.startsWith("x:")) { - defNode = tagInfo.getXDslDefNode(); - } - if (defNode != null) { - for (IXDefAttribute defAttr : defNode.getAttributes().values()) { - String attrName = defAttr.getName(); - if (attrName.startsWith(prefix) && tag.getAttribute(attrName) == null) { - result.addElement(buildAttr(attrName, defNode)); - } - } - } - result.stopHere(); + } // + else if (elType == XmlElementType.XML_ATTRIBUTE) { + XLangAttribute attr = (XLangAttribute) element; + + XLangTag tag = attr.getParentTag(); + if (tag == null) { + return; + } + + String prefix = result.getPrefixMatcher().getPrefix(); + IXDefNode defNode = tag.getSchemaDefNode(); + if (prefix.startsWith(XDslKeys.DEFAULT.X_NS_PREFIX)) { + defNode = tag.getXDslDefNode(); + } + + if (defNode == null) { + return; } - } else if (elType == XmlElementType.XML_ATTRIBUTE_VALUE) { - XmlAttributeValue value = (XmlAttributeValue) element; - String attrName = XmlPsiHelper.getAttrName(value); - XmlTag tag = XmlPsiHelper.getXmlTag(element); - if (tag != null) { - XmlTagInfo tagInfo = XDefPsiHelper.getTagInfo(tag); - if (tagInfo == null || tagInfo.getDefNode() == null) - return; - - IXDefAttribute attr = tagInfo.getDefNode().getAttribute(attrName); - if (attr != null) { - completeAttrValue(result, attr); + + for (IXDefAttribute defAttr : defNode.getAttributes().values()) { + String attrName = defAttr.getName(); + + if (attrName.startsWith(prefix) && tag.getAttribute(attrName) == null) { + result.addElement(buildAttr(attrName, defNode)); } + } + + result.stopHere(); + } // + else if (elType == XmlElementType.XML_ATTRIBUTE_VALUE) { + XLangAttributeValue value = (XLangAttributeValue) element; + + XLangAttribute attr = value.getParentAttr(); + XLangTag tag = attr != null ? attr.getParentTag() : null; + if (tag == null) { + return; + } - result.stopHere(); + String attrName = attr.getName(); + IXDefAttribute defAttr = tag.getSchemaDefNodeAttr(attrName); + if (defAttr != null) { + completeAttrValue(result, defAttr); } + + result.stopHere(); } } private void completeAttrValue(CompletionResultSet result, IXDefAttribute attr) { String prefix = result.getPrefixMatcher().getPrefix(); + XDefTypeDecl type = attr.getType(); - if ("boolean".equals(type.getStdDomain())) { + String stdDomain = type.getStdDomain(); + if ("boolean".equals(stdDomain)) { if ("false".startsWith(prefix)) { result.addElement(buildAttrValue("false", null)); } if ("true".startsWith(prefix)) { result.addElement(buildAttrValue("true", null)); } - } else if (XDefConstants.STD_DOMAIN_ENUM.equals(type.getStdDomain())) { + } // + else if (XDefConstants.STD_DOMAIN_ENUM.equals(stdDomain)) { String dictName = type.getOptions(); - if (dictName != null) { - DictBean dict = DictProvider.instance().getDict(null, dictName, null,null); - if (dict != null && dict.getOptions() != null) { - for (DictOptionBean optionBean : dict.getOptions()) { - if (optionBean.isInternal()) - continue; - result.addElement(buildAttrValue( - ConvertHelper.toString(optionBean.getValue(), ""), optionBean.getLabel())); - } + if (dictName == null) { + return; + } + + DictBean dict = DictProvider.instance().getDict(null, dictName, null, null); + if (dict == null || dict.getOptions() == null) { + return; + } + + for (DictOptionBean optionBean : dict.getOptions()) { + if (optionBean.isInternal()) { + continue; } + result.addElement(buildAttrValue(ConvertHelper.toString(optionBean.getValue(), ""), + optionBean.getLabel())); } } } LookupElement buildTag(String tagName, IXDefNode defNode) { String label = null; + IXDefComment comment = defNode.getComment(); if (comment != null) { label = comment.getMainDisplayName(); @@ -199,26 +231,30 @@ public class XLangCompletionContributor extends CompletionContributor implements // .withPresentableText("Presentable text") // .withItemTextForeground(JBColor.RED) // .bold() - .withIcon(PlatformIcons.VARIABLE_ICON) - .withTailText(label) - //.withTypeText(desc, PlatformIcons.CLASS_ICON, true) - .withInsertHandler(new XmlTagInsertHandler()) - .withTypeIconRightAligned(true); + .withIcon(PlatformIcons.VARIABLE_ICON) + .withTailText(label) + //.withTypeText(desc, PlatformIcons.CLASS_ICON, true) + .withInsertHandler(new XmlTagInsertHandler()) + .withTypeIconRightAligned(true); return e; } LookupElement buildAttr(String attrName, IXDefNode defNode) { String label = null; IXDefComment comment = defNode.getComment(); + if (comment != null) { label = comment.getSubDisplayName(attrName); } - return LookupElementBuilder.create(attrName).withTailText(label).withInsertHandler(new XmlAttributeInsertHandler()); + return LookupElementBuilder.create(attrName) + .withTailText(label) + .withInsertHandler(new XmlAttributeInsertHandler()); } LookupElement buildAttrValue(String value, String label) { - if (Objects.equals(label, value)) + if (Objects.equals(label, value)) { label = null; + } return LookupElementBuilder.create(value).withTailText(label); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java index a09217f52..3208ee20f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java @@ -97,17 +97,17 @@ public class XLangDocumentationProvider extends AbstractDocumentationProvider { return null; } - IXDefAttribute attrDef = attr.getDefAttr(); - XDefTypeDecl attrDefType = attrDef != null ? attrDef.getType() : null; - if (attrDefType == null) { + IXDefAttribute defAttr = attr.getDefAttr(); + XDefTypeDecl defAttrType = defAttr != null ? defAttr.getType() : null; + if (defAttrType == null) { return null; } - if (!XDefConstants.STD_DOMAIN_DICT.equals(attrDefType.getStdDomain())) { + if (!XDefConstants.STD_DOMAIN_DICT.equals(defAttrType.getStdDomain())) { return null; } - DictBean dictBean = loadDict(element, attrDefType.getOptions()); + DictBean dictBean = loadDict(element, defAttrType.getOptions()); DictOptionBean option = dictBean != null ? dictBean.getOptionByValue(attr.getValue()) : null; if (option == null) { return null; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java index 13f84776e..7cfb09933 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java @@ -52,8 +52,8 @@ public class XLangDocumentation { this(defNode, defNode.getXdefValue()); } - public XLangDocumentation(IXDefAttribute attrDef) { - this(attrDef, attrDef.getType()); + public XLangDocumentation(IXDefAttribute defAttr) { + this(defAttr, defAttr.getType()); } XLangDocumentation(ISourceLocationGetter locGetter, XDefTypeDecl type) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java index c6e1bf66b..cc0e09e1c 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -13,7 +13,7 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.SchemaPrefixReference; import com.intellij.psi.impl.source.xml.XmlAttributeImpl; -import io.nop.idea.plugin.lang.reference.XLangDefAttrReference; +import io.nop.idea.plugin.lang.reference.XLangAttributeReference; import io.nop.xlang.xdef.IXDefAttribute; import org.jetbrains.annotations.NotNull; @@ -51,7 +51,7 @@ public class XLangAttribute extends XmlAttributeImpl { int nameOffset = (ns.isEmpty() ? -1 : ns.length()) + 1; TextRange nameTextRange = TextRange.allOf(name).shiftRight(nameOffset); - XLangDefAttrReference ref1 = new XLangDefAttrReference(this, nameTextRange); + XLangAttributeReference ref1 = new XLangAttributeReference(this, nameTextRange); return ref0 != null ? new PsiReference[] { ref0, ref1 } : new PsiReference[] { ref1 }; } @@ -67,15 +67,15 @@ public class XLangAttribute extends XmlAttributeImpl { String attrName = getName(); boolean hasXDslNs = !ns.isEmpty() && ns.equals(tag.getXDslKeys().NS); - IXDefAttribute attrDef; + IXDefAttribute defAttr; // 取 xdsl.xdef 中声明的属性 if (hasXDslNs) { - attrDef = tag.getXDslDefNodeAttr(attrName); + defAttr = tag.getXDslDefNodeAttr(attrName); } // else { - attrDef = tag.getSchemaDefNodeAttr(attrName); + defAttr = tag.getSchemaDefNodeAttr(attrName); } - return attrDef; + return defAttr; } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index 681606b32..8a5e126cd 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -60,9 +60,9 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { return PsiReference.EMPTY_ARRAY; } - IXDefAttribute attrDef = attr.getDefAttr(); + IXDefAttribute defAttr = attr.getDefAttr(); // 对于未定义属性,不做引用识别 - if (attrDef == null) { + if (defAttr == null) { return PsiReference.EMPTY_ARRAY; } @@ -73,7 +73,7 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { } // 根据属性定义类型,从属性值中查找引用 - refs = XLangReferenceHelper.getReferencesByDefType(this, attrValue, attrDef.getType()); + refs = XLangReferenceHelper.getReferencesByDefType(this, attrValue, defAttr.getType()); if (refs != null) { return refs; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 9ac35571c..30ae36d47 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -358,18 +358,18 @@ public class XLangTag extends XmlTagImpl { } } - IXDefAttribute attrDef = getXDefNodeAttr(defNode, attrName); - if (attrDef == null) { + IXDefAttribute defAttr = getXDefNodeAttr(defNode, attrName); + if (defAttr == null) { return null; } - XLangDocumentation doc = new XLangDocumentation(attrDef); + XLangDocumentation doc = new XLangDocumentation(defAttr); doc.setMainTitle(mainTitle); IXDefComment nodeComment = defNode.getComment(); if (nodeComment != null) { IXDefSubComment attrComment = nodeComment.getSubComments().get(attrName); - if (attrComment == null && attrDef.isUnknownAttr()) { + if (attrComment == null && defAttr.isUnknownAttr()) { attrComment = nodeComment.getSubComments().get(XDefKeys.DEFAULT.UNKNOWN_ATTR); } @@ -442,7 +442,7 @@ public class XLangTag extends XmlTagImpl { return null; } - private static String changeNamespace(String name, String fromNs, String toNs) { + public static String changeNamespace(String name, String fromNs, String toNs) { if (fromNs == null || toNs == null || fromNs.equals(toNs)) { return name; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java new file mode 100644 index 000000000..3f1710c4e --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang.reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import com.intellij.codeInsight.completion.XmlAttributeInsertHandler; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.util.PlatformIcons; +import io.nop.api.core.util.SourceLocation; +import io.nop.idea.plugin.lang.psi.XLangAttribute; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import io.nop.xlang.xdef.IXDefAttribute; +import io.nop.xlang.xdef.IXDefComment; +import io.nop.xlang.xdef.IXDefNode; +import io.nop.xlang.xdef.XDefKeys; +import io.nop.xlang.xdsl.XDslKeys; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对 {@link XLangAttributeReference} 的引用识别:指向属性的定义位置 + * + * @author flytreeleft + * @date 2025-07-10 + */ +public class XLangAttributeReference extends XLangReferenceBase { + + public XLangAttributeReference(XLangAttribute myElement, TextRange myRangeInElement) { + super(myElement, myRangeInElement); + } + + @Override + public @Nullable PsiElement resolveInner() { + XLangAttribute attr = (XLangAttribute) myElement; + IXDefAttribute defAttr = attr.getDefAttr(); + if (defAttr == null) { + return null; + } + + String path = XmlPsiHelper.getNopVfsPath(defAttr); + if (path == null) { + return null; + } + + SourceLocation loc = defAttr.getLocation(); + Function targetResolver = (file) -> { + PsiElement target = XmlPsiHelper.getPsiElementAt(file, loc, XLangAttribute.class); + + if (target == null) { + target = XmlPsiHelper.getPsiElementAt(file, loc, XLangTag.class); + + if (target instanceof XLangTag tag) { + // Note: 在交叉定义时,属性定义中的属性名字与当前属性名字是不相同的 + target = tag.getAttribute(defAttr.getName()); + } + } + return target; + }; + + return new NopVirtualFile(myElement, path, targetResolver); + } + + @Override + public Object @NotNull [] getVariants() { + XLangAttribute attr = (XLangAttribute) myElement; + XLangTag tag = attr.getParentTag(); + if (tag == null) { + return LookupElement.EMPTY_ARRAY; + } + + String attrNs = attr.getNamespacePrefix(); + XDefKeys xdefKeys = tag.getXDefKeys(); + XDslKeys xdslKeys = tag.getXDslKeys(); + // Note: 需支持处理 x/xdef 的名字空间非默认的情况 + boolean usedXDefNs = xdefKeys.NS.equals(attrNs); + boolean usedXDslNs = xdslKeys.NS.equals(attrNs); + + IXDefNode xdslDefNode = tag.getXDslDefNode(); + IXDefNode tagDefNode = tag.getSchemaDefNode(); + if (usedXDslNs) { + tagDefNode = xdslDefNode; + } + + if (tagDefNode == null) { + return LookupElement.EMPTY_ARRAY; + } + + List result = new ArrayList<>(); + Set existAttrNames = XmlPsiHelper.getTagAttrNames(tag); + + String stdNs = usedXDefNs ? XDefKeys.DEFAULT.NS // + : usedXDslNs // + ? XDslKeys.DEFAULT.NS : attrNs; + addDefAttr(result, tagDefNode, stdNs, existAttrNames); + // xdsl.xdef 的节点属性对于 DSL 始终可用 + if (!usedXDslNs && xdslDefNode != null) { + addDefAttr(result, xdslDefNode, attrNs, existAttrNames); + } + + return result.stream() // + .sorted((a, b) -> XLangTagReference.NAME_COMPARATOR.compare(a.name, b.name)) // + .map((defAttr) -> { + boolean trimNs = !attrNs.isEmpty(); + + String attrName = defAttr.name; + if (!trimNs) { + attrName = XLangTag.changeNamespace(attrName, XDefKeys.DEFAULT.NS, xdefKeys.NS); + attrName = XLangTag.changeNamespace(attrName, XDslKeys.DEFAULT.NS, xdslKeys.NS); + } + + return lookupAttr(attrName, defAttr.label, trimNs); + }) // + .toArray(LookupElement[]::new); + } + + private static void addDefAttr( + List list, IXDefNode defNode, String onlyNs, Set excludeNames + ) { + for (IXDefAttribute defAttr : defNode.getAttributes().values()) { + String attrName = defAttr.getName(); + + if ((!onlyNs.isEmpty() && !attrName.startsWith(onlyNs + ':')) // + || excludeNames.contains(attrName) // + ) { + continue; + } + + String label = null; + IXDefComment comment = defNode.getComment(); + if (comment != null) { + label = comment.getSubDisplayName(attrName); + } + + list.add(new DefAttrWithLabel(attrName, defAttr, label)); + } + } + + /** 注意,若当前属性已经包含完整的名字空间,则补全项必须移除其名字空间,否则,补全项的插入位置将会发生偏移 */ + private static LookupElement lookupAttr(String attrName, String label, boolean trimNs) { + if (trimNs) { + attrName = attrName.substring(attrName.indexOf(':') + 1); + } + + return LookupElementBuilder.create(attrName) + // icon 靠左布局 + .withIcon(PlatformIcons.PROPERTY_ICON) + // type text 靠后布局 + .withTypeText(label) +// // tail text 与 lookup string 紧挨着 +// .withTailText(label) // +// // presentable text 将替换 lookup string 作为最终的显示文档 +// .withPresentableText(label) // + .withInsertHandler(new XmlAttributeInsertHandler()); + } + + private record DefAttrWithLabel(String name, IXDefAttribute def, String label) {} +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java deleted file mode 100644 index bd1258ac5..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDefAttrReference.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2017-2025 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ - -package io.nop.idea.plugin.lang.reference; - -import java.util.function.Function; - -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import io.nop.api.core.util.SourceLocation; -import io.nop.idea.plugin.lang.psi.XLangAttribute; -import io.nop.idea.plugin.lang.psi.XLangTag; -import io.nop.idea.plugin.utils.XmlPsiHelper; -import io.nop.idea.plugin.vfs.NopVirtualFile; -import io.nop.xlang.xdef.IXDefAttribute; -import org.jetbrains.annotations.Nullable; - -/** - * 对属性定义的引用:指向属性的定义位置 - * - * @author flytreeleft - * @date 2025-07-10 - */ -public class XLangDefAttrReference extends XLangReferenceBase { - - public XLangDefAttrReference(XLangAttribute myElement, TextRange myRangeInElement) { - super(myElement, myRangeInElement); - } - - @Override - public @Nullable PsiElement resolveInner() { - XLangAttribute attr = (XLangAttribute) myElement; - IXDefAttribute attrDef = attr.getDefAttr(); - if (attrDef == null) { - return null; - } - - String path = XmlPsiHelper.getNopVfsPath(attrDef); - if (path == null) { - return null; - } - - SourceLocation loc = attrDef.getLocation(); - Function targetResolver = (file) -> { - PsiElement target = XmlPsiHelper.getPsiElementAt(file, loc, XLangAttribute.class); - - if (target == null) { - target = XmlPsiHelper.getPsiElementAt(file, loc, XLangTag.class); - - if (target instanceof XLangTag tag) { - // Note: 在交叉定义时,属性定义中的属性名字与当前属性名字是不相同的 - target = tag.getAttribute(attrDef.getName()); - } - } - return target; - }; - - return new NopVirtualFile(myElement, path, targetResolver); - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java index 93f3c9d1a..0fe06da99 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java @@ -8,25 +8,53 @@ package io.nop.idea.plugin.lang.reference; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; import java.util.function.Function; +import com.intellij.codeInsight.completion.XmlTagInsertHandler; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.util.PlatformIcons; import io.nop.api.core.util.SourceLocation; import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFile; +import io.nop.xlang.xdef.IXDefComment; import io.nop.xlang.xdef.IXDefNode; +import io.nop.xlang.xdsl.XDslKeys; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** - * 对节点定义的引用:指向节点的定义位置 + * 对 {@link XLangTag} 的引用识别:指向节点的定义位置 * * @author flytreeleft * @date 2025-07-17 */ public class XLangTagReference extends XLangReferenceBase { + public static final Comparator NAME_COMPARATOR = (a, b) -> { + int aNsIndex = a.indexOf(':'); + int bNsIndex = b.indexOf(':'); + + // 确保无命名空间的属性排在最前面,且 xdef 名字空间排在其他名字空间之前 + if (aNsIndex <= 0 && bNsIndex <= 0) { + return a.compareTo(b); + } // + else if (aNsIndex > 0 && bNsIndex > 0) { + return !a.startsWith("xdef:") && b.startsWith("xdef:") + ? 1 + : a.startsWith("xdef:") && !b.startsWith("xdef:") // + ? -1 : a.compareTo(b); + } + + return Integer.compare(aNsIndex, bNsIndex); + }; public XLangTagReference(XLangTag myElement, TextRange myRangeInElement) { super(myElement, myRangeInElement); @@ -49,4 +77,85 @@ public class XLangTagReference extends XLangReferenceBase { return new NopVirtualFile(myElement, path, targetResolver); } + + @Override + public Object @NotNull [] getVariants() { + XLangTag tag = (XLangTag) myElement; + XLangTag parentTag = tag.getParentTag(); + if (parentTag == null) { + return LookupElement.EMPTY_ARRAY; + } + + String tagNs = tag.getNamespacePrefix(); + boolean usedXDslNs = XDslKeys.DEFAULT.NS.equals(tagNs); + + IXDefNode xdslDefNode = parentTag.getXDslDefNode(); + IXDefNode parentTagDefNode = parentTag.getSchemaDefNode(); + if (usedXDslNs) { + parentTagDefNode = xdslDefNode; + } + + if (parentTagDefNode == null) { + return LookupElement.EMPTY_ARRAY; + } + + List result = new ArrayList<>(); + Set existChildTagNames = XmlPsiHelper.getChildTagNames(parentTag); + + addChildDefNode(result, parentTagDefNode, tagNs, existChildTagNames); + // xdsl.xdef 的节点对于 DSL 始终可用 + if (!usedXDslNs && xdslDefNode != null) { + addChildDefNode(result, xdslDefNode, tagNs, existChildTagNames); + } + + return result.stream() + .sorted((a, b) -> NAME_COMPARATOR.compare(a.getTagName(), b.getTagName())) + .map((defNode) -> lookupTag(defNode, !tagNs.isEmpty())) + .toArray(LookupElement[]::new); + } + + private static void addChildDefNode( + List list, IXDefNode parentDefNode, String onlyNs, Set excludeNames + ) { + for (IXDefNode defNode : parentDefNode.getChildren().values()) { + String tagName = defNode.getTagName(); + + if ((!onlyNs.isEmpty() && !tagName.startsWith(onlyNs + ':')) // + || defNode.isInternal() // + || (!defNode.isAllowMultiple() // + && excludeNames.contains(tagName) // + ) // + ) { + continue; + } + + list.add(defNode); + } + } + + /** 注意,若当前标签已经包含完整的名字空间,则补全项必须移除其名字空间,否则,补全项的插入位置将会发生偏移 */ + private static LookupElement lookupTag(IXDefNode defNode, boolean trimNs) { + String label = null; + + IXDefComment comment = defNode.getComment(); + if (comment != null) { + label = comment.getMainDisplayName(); + } + + String tagName = defNode.getTagName(); + if (trimNs) { + tagName = tagName.substring(tagName.indexOf(':') + 1); + } + + return LookupElementBuilder.create(tagName) + // icon 靠左布局 + .withIcon(PlatformIcons.XML_TAG_ICON) + // type text 靠后布局 + .withTypeText(label) +// // tail text 与 lookup string 紧挨着 +// .withTailText(label) // +// // presentable text 将替换 lookup string 作为最终的显示文档 +// .withPresentableText(label) // + .withInsertHandler(new XmlTagInsertHandler()); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index 3b1cb2927..2ccea4ffe 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java @@ -7,20 +7,14 @@ */ package io.nop.idea.plugin.utils; -import java.util.ArrayList; -import java.util.List; - -import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.xml.XmlTag; -import io.nop.commons.util.CollectionHelper; import io.nop.commons.util.StringHelper; import io.nop.core.resource.IResource; import io.nop.core.resource.impl.ClassPathResource; import io.nop.core.resource.impl.InMemoryTextResource; import io.nop.xlang.xdef.IXDefNode; import io.nop.xlang.xdef.IXDefinition; -import io.nop.xlang.xdef.XDefKeys; import io.nop.xlang.xdef.parse.XDefinitionParser; import io.nop.xlang.xdsl.XDslConstants; import io.nop.xlang.xdsl.XDslKeys; @@ -28,8 +22,6 @@ import io.nop.xlang.xmeta.SchemaLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static io.nop.idea.plugin.utils.XmlPsiHelper.getXmlTag; - public class XDefPsiHelper { static final Logger LOG = LoggerFactory.getLogger(XDefPsiHelper.class); @@ -72,16 +64,6 @@ public class XDefPsiHelper { return ns; } - public static String getXDefNamespace(XmlTag tag) { - XmlTag rootTag = XmlPsiHelper.getRoot(tag); - String ns = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDEF); - - if (ns == null) { - ns = XDefKeys.DEFAULT.NS; - } - return ns; - } - /** 从根节点获取 dsl 的元模型的 vfs 路径 */ public static String getSchemaPath(XmlTag rootTag) { PsiFile file = rootTag.getContainingFile(); @@ -120,96 +102,6 @@ public class XDefPsiHelper { } } - public static XmlTagInfo getTagInfo(PsiElement element) { - XmlTag tag = getXmlTag(element); - if (tag == null) { - return null; - } - - String schemaUrl = getSchemaPath(XmlPsiHelper.getRoot(tag)); - if (schemaUrl != null) { - return getTagInfo(schemaUrl, tag); - } - return null; - } - - public static XmlTagInfo getTagInfo(String schemaUrl, XmlTag tag) { - IXDefinition def = loadSchema(schemaUrl); - if (def == null) { - return null; - } - - IXDefNode xdslDefNode = getXDslDef().getRootNode(); - - List tags = getSelfAndParents(tag); - tags = CollectionHelper.reverseList(tags); - - XmlTag rootTag = tags.get(0); - String xdefNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDEF); - String xdslNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); - - boolean xpl = false; - boolean xlibDsl = XDslConstants.XDSL_SCHEMA_XLIB.equals(schemaUrl); - XmlTagInfo tagInfo = null; - for (int i = 0, n = tags.size(); i < n; i++) { - XmlTag xmlTag = tags.get(i); - - if (i == 0) { - tagInfo = new XmlTagInfo(xmlTag, null, def, def.getRootNode(), // - xdslDefNode, xdefNs, xdslNs); - } else { - XmlTagInfo parentTagInfo = tagInfo; - String tagName = normalizeNamespace(xmlTag.getName(), xdefNs, xdslNs); - - xdslDefNode = parentTagInfo.getXDslDefNodeChild(tagName); - - IXDefNode defNode; - // Note: 只有不在 xdsl.xdef 中,且以 x 为名字空间的节点,才使用 xdsl 节点定义, - // 否则,保持在 XDef 元模型的节点定义 - if (tagName.startsWith("x:") && "x".equals(xdslNs)) { - defNode = xdslDefNode; - } - // Xpl 节点始终采用 xpl.xdef 元模型 - else if (xpl) { - // 通过任意未定义的子节点名称,得到 xpl 的 xdef:unknown-tag 子节点定义 - defNode = getXplDef().getRootNode().getChild("any"); - } else { - defNode = parentTagInfo.getDefNodeChild(tagName); - } - - tagInfo = new XmlTagInfo(xmlTag, parentTagInfo, def, defNode, xdslDefNode, xdefNs, xdslNs); - - if (isXplDefNode(defNode)) { - xpl = true; - } - // xlib.xdef 中的 source 标签设置为 xml 类型,是因为在获取 XplLib 模型的时候会根据 xlib.xdef 来解析, - // 但此时这个 source 段无法自动进行编译,必须结合它的 outputMode 和 attrs 配置等才能决定。 - // 因此,将其子节点同样视为 xpl 节点处理 - else if (!xpl && xlibDsl && "source".equals(tagName) // - && "xml".equals(getDefNodeType(defNode)) // - && parentTagInfo.getDefNode().isUnknownTag() // - && "tags".equals(parentTagInfo.getParentDefNode().getTagName()) // - ) { - xpl = true; - } - } - } - return tagInfo; - } - - /** 确保 `xmlName` 的 `xdef.xdef`、`xdsl.xdef` 对应的名字空间始终为 `xdef` 和 `x` */ - public static String normalizeNamespace(String xmlName, String xdefNs, String xdslNs) { - // 转换 /nop/schema/xdsl.xdef 的 xdsl 名字空间 - if (xdslNs != null && !"x".equals(xdslNs) && xmlName.startsWith(xdslNs + ":")) { - xmlName = "x:" + xmlName.substring((xdslNs + ":").length()); - } - // 转换 /nop/schema/xdef.xdef 的 meta 名字空间 - else if (xdefNs != null && !"xdef".equals(xdefNs) && xmlName.startsWith(xdefNs + ":")) { - xmlName = "xdef:" + xmlName.substring((xdefNs + ":").length()); - } - return xmlName; - } - public static boolean isXplDefNode(IXDefNode defNode) { String stdDomain = getDefNodeType(defNode); @@ -222,15 +114,4 @@ public class XDefPsiHelper { } return defNode.getXdefValue().getStdDomain(); } - - /** 自底向上查找 tag 所在分支上的节点 */ - static List getSelfAndParents(XmlTag tag) { - List ret = new ArrayList<>(); - - while (tag != null) { - ret.add(tag); - tag = tag.getParentTag(); - } - return ret; - } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index e83183af3..f3c01678d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -17,7 +17,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.Predicate; -import com.intellij.lang.ASTNode; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; @@ -31,7 +30,6 @@ import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTokenType; @@ -59,11 +57,13 @@ public class XmlPsiHelper { } VirtualFile vf = file.getVirtualFile(); - if (vf == null) { - return null; + // Note: 在编辑过程中得到的 VirtualFile 可能为 null,需尝试通过 + // PsiFile#getOriginalFile 获得 VirtualFile + if (vf == null && file.getOriginalFile() != file) { + vf = file.getOriginalFile().getVirtualFile(); } - return ProjectFileHelper.getNopVfsPath(vf); + return vf != null ? ProjectFileHelper.getNopVfsPath(vf) : null; } /** @@ -241,19 +241,6 @@ public class XmlPsiHelper { return SourceLocation.fromLine(path, sourceLine, sourceColumn, len); } - public static String getAttrName(XmlAttributeValue value) { - if (value == null) { - return null; - } - - if (!(value.getParent() instanceof XmlAttribute)) { - return null; - } - - XmlAttribute attr = (XmlAttribute) value.getParent(); - return attr.getName(); - } - public static boolean isInComment(PsiElement element) { IElementType elementType = element.getNode().getElementType(); return elementType == XmlTokenType.XML_COMMENT_END @@ -261,23 +248,24 @@ public class XmlPsiHelper { || elementType == XmlTokenType.XML_COMMENT_CHARACTERS; } - public static boolean isElementType(PsiElement element, IElementType type) { - ASTNode node = element != null ? element.getNode() : null; - if (node == null) { - return false; - } - - return node.getElementType() == type; - } - public static Set getChildTagNames(XmlTag tag) { - Set tagNames = new HashSet<>(); + Set names = new HashSet<>(); + for (PsiElement element : tag.getChildren()) { if (element instanceof XmlTag) { - tagNames.add(((XmlTag) element).getName()); + names.add(((XmlTag) element).getName()); } } - return tagNames; + return names; + } + + public static Set getTagAttrNames(XmlTag tag) { + Set names = new HashSet<>(); + + for (XmlAttribute attr : tag.getAttributes()) { + names.add(attr.getName()); + } + return names; } /** @@ -335,27 +323,6 @@ public class XmlPsiHelper { return (T) result[0]; } - public static XmlTag getXmlTag(PsiElement element) { - if (element == null) { - return null; - } - - if (element instanceof XmlTag) { - return ((XmlTag) element); - } - - do { - PsiElement parent = element.getParent(); - if (parent == null) { - return null; - } - if (parent instanceof XmlTag) { - return (XmlTag) parent; - } - element = parent; - } while (true); - } - public static XmlTag getRoot(XmlTag tag) { do { XmlTag parent = tag.getParentTag(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java deleted file mode 100644 index e8fbcc310..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ -package io.nop.idea.plugin.utils; - -import com.intellij.psi.xml.XmlTag; -import io.nop.commons.util.StringHelper; -import io.nop.xlang.xdef.IXDefAttribute; -import io.nop.xlang.xdef.IXDefComment; -import io.nop.xlang.xdef.IXDefNode; -import io.nop.xlang.xdef.IXDefSubComment; -import io.nop.xlang.xdef.IXDefinition; -import io.nop.xlang.xdef.XDefConstants; -import io.nop.xlang.xdef.XDefTypeDecl; -import io.nop.xlang.xdef.domain.StdDomainRegistry; -import io.nop.xlang.xdef.impl.XDefAttribute; -import io.nop.xlang.xdef.parse.XDefTypeDeclParser; -import io.nop.xlang.xdsl.XDslConstants; - -public class XmlTagInfo { - private final XmlTag tag; - - private final IXDefinition def; - private final IXDefNode defNode; - private final IXDefNode xdslDefNode; - - private final IXDefNode parentDefNode; - private final String xdefNs; - private final String xdslNs; - - private final boolean custom; - - public XmlTagInfo( - XmlTag tag, XmlTagInfo parentTagInfo, // - IXDefinition def, IXDefNode defNode, // - IXDefNode xdslDefNode, // - String xdefNs, String xdslNs // - ) { - this.tag = tag; - this.def = def; - this.defNode = defNode; - this.xdslDefNode = xdslDefNode; - - this.parentDefNode = parentTagInfo != null ? parentTagInfo.getDefNode() : null; - this.xdefNs = xdefNs; - this.xdslNs = xdslNs; - - this.custom = parentTagInfo != null // - && (parentTagInfo.isCustom() || parentTagInfo.isSupportBody()); - } - - /** 获取当前节点的 xml 标签 */ - public XmlTag getTag() { - return tag; - } - - /** 获取当前节点所在 DSL 的 xdef 定义 */ - public IXDefinition getDef() { - return def; - } - - /** 获取当前节点的 xdef 定义 */ - public IXDefNode getDefNode() { - return defNode; - } - - /** 获取当前节点指定子节点的 xdef 定义 */ - public IXDefNode getDefNodeChild(String tagName) { - if (defNode == null) { - return null; - } - return defNode.getChild(tagName); - } - - /** 获取当前节点父节点的 xdef 定义 */ - public IXDefNode getParentDefNode() { - return parentDefNode; - } - - /** 获取当前节点在 xdsl.xdef 中所对应节点的 xdef 定义 */ - public IXDefNode getXDslDefNode() { - return xdslDefNode; - } - - /** 获取当前节点在 xdsl.xdef 中所对应节点的指定子节点的 xdef 定义 */ - public IXDefNode getXDslDefNodeChild(String tagName) { - if (xdslDefNode == null) { - return null; - } - return xdslDefNode.getChild(tagName); - } - - public boolean isCustom() { - return custom; - } - - public boolean isSupportBody() { - if (defNode == null) { - return false; - } - if (defNode.getXdefValue() == null) { - return false; - } - return defNode.getXdefValue().isSupportBody(StdDomainRegistry.instance()); - } - - public boolean isAllowedUnknownName(String name) { - if (!StringHelper.hasNamespace(name)) { - return false; - } - - String ns = StringHelper.getNamespace(name); - if (def.getXdefCheckNs() == null || def.getXdefCheckNs().isEmpty()) { - return true; - } - - return !def.getXdefCheckNs().contains(ns); - } - - /** - * 判断当前节点上的指定属性是否为 XDef 元模型的元属性(即,定义属性名及其类型) - *

- * 对于这类属性,仅做类型引用跳转,不做文件或名字引用跳转 - */ - public boolean isDefDeclaredAttr(String attrName) { - // Note: - // - 自定义节点(包括 Xpl 类型节点及其子节点)没有元模型 - // - 在 DSL 节点上的属性也不是元属性 - if (isCustom() || isDslNode()) { - return false; - } - - // 检查在 *.xdef 节点上的元属性 - String ns = StringHelper.getNamespace(attrName); - if (StringHelper.isEmpty(ns)) { - return true; - } - - // xdef.xdef 中 xdef 名字空间的属性均视为元属性 - if (isXDefNode()) { - return "xdef".equals(ns); - } - // xdsl.xdef 中 x 名字空间的属性均视为元属性 - else if (isXDslNode()) { - return "x".equals(ns); - } - // 对于普通的 *.xdef,除 xmlns、xdef、x 名字空间以外的属性,均视为元属性 - return !ns.equals("xdef") && !ns.equals("x") && !ns.equals("xmlns"); - } - - /** 获取当前节点上指定属性的 xdef 定义 */ - public IXDefAttribute getDefAttr(String attrName) { - DefAttrWithNode attr = getDefAttrInfo(attrName); - - return attr != null ? attr.attr : null; - } - - /** 获取当前节点上指定属性的类型 */ - public XDefTypeDecl getDefAttrType(String attrName) { - IXDefAttribute attr = getDefAttr(attrName); - - return attr != null ? attr.getType() : null; - } - - /** 获取节点注释 */ - public IXDefComment getDefNodeComment() { - return defNode != null ? defNode.getComment() : null; - } - - /** 获取指定属性的注释 */ - public IXDefSubComment getDefAttrComment(String attrName) { - if (isXmlns(attrName)) { - return null; - } - - IXDefComment comment = null; - DefAttrWithNode attr = getDefAttrInfo(attrName); - if (attr != null) { - comment = attr.node.getComment(); - attrName = attr.attr.isUnknownAttr() ? "xdef:unknown-attr" : attr.attr.getName(); - } - - return comment != null ? comment.getSubComments().get(attrName) : null; - } - - private boolean isXmlns(String name) { - return name.equals("xmlns") || name.startsWith("xmlns:"); - } - - /** 当前节点是否为 DSL 节点 */ - private boolean isDslNode() { - return !XDslConstants.XDSL_SCHEMA_XDEF.equals(def.resourcePath()); - } - - /** 是否为 xdef.xdef 中的节点 */ - private boolean isXDefNode() { - return xdefNs != null && !"xdef".equals(xdefNs); - } - - /** 是否为 xdsl.xdef 中的节点 */ - private boolean isXDslNode() { - return xdslNs != null && !"x".equals(xdslNs); - } - - /** 记录属性所在节点 */ - record DefAttrWithNode(IXDefNode node, IXDefAttribute attr) {} - - /** 获取当前节点上指定属性的定义信息 */ - private DefAttrWithNode getDefAttrInfo(String attrName) { - // 为 xmlns 节点构造属性 - if (isXmlns(attrName)) { - String attrValue = tag.getAttributeValue(attrName); - // 忽略 xmlns:biz="biz" 形式的属性 - if (attrName.endsWith(":" + attrValue)) { - return null; - } - - XDefTypeDecl type = new XDefTypeDeclParser().parseFromText(null, XDefConstants.STD_DOMAIN_XDEF_REF); - - XDefAttribute attr = new XDefAttribute(); - attr.setName(attrName); - attr.setType(type); - - return defNode != null ? new DefAttrWithNode(defNode, attr) : null; - } - - attrName = XDefPsiHelper.normalizeNamespace(attrName, xdefNs, xdslNs); - - // 查找在当前节点上声明的属性 - IXDefAttribute attr = defNode != null ? defNode.getAttribute(attrName) : null; - if (attr != null) { - return new DefAttrWithNode(defNode, attr); - } - - // 查找在对应的 xdsl.xdef 节点上声明的属性 - attr = xdslDefNode != null ? xdslDefNode.getAttribute(attrName) : null; - if (attr != null) { - return new DefAttrWithNode(xdslDefNode, attr); - } - - // 查找当前节点上声明的 xdef:unknown-attr 属性: - // 在当前 dsl 为 *.xdef(即,x:schema 为 /nop/schema/xdef.xdef)时有效 - attr = defNode != null ? defNode.getAttribute("xdef:unknown-attr") : null; - if (attr != null) { - return new DefAttrWithNode(defNode, attr); - } - - // 针对 xdef.xdef 中的未确定属性:本质上都是 XDefNode 节点上的属性 - if (isXDefNode()) { - IXDefNode node = def.getXdefUnknownTag(); - - return new DefAttrWithNode(node, node.getAttribute(attrName)); - } - - // Note: 在普通 *.xdef 的 IXDefNode 中, - // 对 xdef:unknown-attr 只记录了类型,并没有 IXDefAttribute 实体, - // 其处理逻辑见 XDefinitionParser#parseNode - XDefTypeDecl xdefUnknownAttrType = defNode != null ? defNode.getXdefUnknownAttr() : null; - if (xdefUnknownAttrType != null) { - XDefAttribute at = new XDefAttribute() { - @Override - public boolean isUnknownAttr() { - return true; - } - }; - - at.setName(attrName); - at.setType(xdefUnknownAttrType); - - return new DefAttrWithNode(defNode, at); - } - - return null; - } -} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 054cf9241..a77eddbcf 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -167,14 +167,14 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur } protected PsiElement getElementAtCaret() { - assertCaretExists(); + doAssertCaretExists(); return myFixture.getFile().findElementAt(myFixture.getCaretOffset()); } /** 找到光标位置的 {@link XLangReference} 或者其他类型的唯一引用 */ protected PsiReference findReferenceAtCaret() { - assertCaretExists(); + doAssertCaretExists(); // 实际有多个引用时,将构造返回 PsiMultiReference, // 其会按 PsiMultiReference#COMPARATOR 对引用排序得到优先引用, @@ -205,12 +205,21 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur return docProvider.generateDoc(element, originalElement); } - private void assertCaretExists() { + private void doAssertCaretExists() { assertTrue("No '' found in current text", myFixture.getCaretOffset() > 0); } /** 检查自动补全所选中的第一个补全项是否与预期相符 */ - protected void assertCompletion(String expectedText) { + protected void doAssertCompletion(String expectedText) { + doAssertCompletion(null, expectedText); + } + + /** 检查选中指定的补全项之后的文本是否与预期相符 */ + protected void doAssertCompletion(String selectedItem, String expectedText) { + // Note: + // - 在仅有唯一的补全元素时,将自动完成补全,且不能再获取到补全列表 + // - 只有在调用 `myFixture.completeBasic()` 后,才能完成补全,获得补全列表 + // 获取当前查找元素 LookupImpl lookup = (LookupImpl) LookupManager.getActiveLookup(myFixture.getEditor()); assertNotNull("Lookup not active", lookup); @@ -219,7 +228,18 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur assertFalse("No completion items", items.isEmpty()); // 选择第一个补全项 - LookupElement item = items.get(0); + LookupElement item = null; + if (selectedItem == null) { + item = items.get(0); + } else { + for (LookupElement i : items) { + if (i.getLookupString().equals(selectedItem)) { + item = i; + break; + } + } + assertNotNull("No completion item matched with '" + selectedItem + "'", item); + } lookup.setCurrentItem(item); // 模拟选中补全项 @@ -232,7 +252,7 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur /** * 检查 {@link PsiElement} 的解析树是否与指定的 vfs 文件 expectedAstFile 的内容相同 */ - protected void assertASTTree(PsiElement tree, String expectedAstFile) { + protected void doAssertASTTree(PsiElement tree, String expectedAstFile) { String testTree = DebugUtil.psiToString(tree, true, false); String expectedTree = readVfsResource(expectedAstFile); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java deleted file mode 100644 index 8631c45bb..000000000 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ -package io.nop.idea.plugin.completion; - -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase; -import org.junit.Test; - -public class TestXLangCompletionContributor extends LightPlatformCodeInsightFixture4TestCase { - @Test - public void noEmptyPrefix() { - // empty file - myFixture.configureByText("test.xgen", ""); - assertEquals("expected no completions for an empty file", - 0, myFixture.completeBasic().length); - - // whitespace suffix - myFixture.configureByText("test.xgen", " a"); - myFixture.type(" "); - assertEquals("expected no completions in content", - 0, myFixture.completeBasic().length); - } - - @Test - public void testCompletion() { - myFixture.configureByText("test.xgen", ""); - - myFixture.type("foo"); - LookupElement[] items = myFixture.completeBasic(); - } -} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java new file mode 100644 index 000000000..8ad4236ca --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ +package io.nop.idea.plugin.lang; + +import java.util.Arrays; + +import io.nop.idea.plugin.BaseXLangPluginTestCase; +import io.nop.idea.plugin.lang.reference.XLangTagReference; + +public class TestXLangCompletions extends BaseXLangPluginTestCase { + + public void testNameSort() { + String[] names = new String[] { + "x:name", // + "x:id", // + "xdef:ref", // + "xui:name", // + "xdef:name", // + "name", // + "xui:label", // + "value", // + }; + + Arrays.sort(names, XLangTagReference.NAME_COMPARATOR); + + assertEquals(String.join(", ", new String[] { + "name", // + "value", // + "xdef:name", // + "xdef:ref", // + "x:id", // + "x:name", // + "xui:label", // + "xui:name", // + }), String.join(", ", names)); + } + + public void testTagCompletion() { + // 从名字空间开始补全 + assertCompletion("xdef:unknown-tag", // + """ + + + + """, // + """ + + + + """); + assertCompletion("x:gen-extends", // + """ + + + + """, // + """ + + + + """); + + // 从名字空间之后补全 + assertCompletion("unknown-tag", // + """ + + + + """, // + """ + + + + """); + assertCompletion("post-parse", // + """ + + + + """, // + """ + + + + """); + + assertCompletion("gen-extends", // + """ + + + + """, // + """ + + + + """); + + // 不带名字空间的补全 + assertCompletion(""" + + + + """, // + """ + + + + """); + } + + public void testAttributeCompletion() { + // 从名字空间开始补全 + assertCompletion("xdsl:dump", // + """ + + > + + """, // + """ + + + """); + assertCompletion("meta:bean-package", // + """ + + > + + """, // + """ + + + """); + + assertCompletion("xdef:default-extends", // + """ + + > + + """, // + """ + + + """); + assertCompletion("x:extends", // + """ + + > + + """, // + """ + + + """); + + assertCompletion("xdef:value", // + """ + + /> + + """, // + """ + + + + """); + assertCompletion("x:override", // + """ + + /> + + """, // + """ + + + + """); + + assertCompletion("xpl:enableNs", // + """ + + + + + + """, // + """ + + + + + """); + + // 从名字空间之后补全 + assertCompletion("dump", // + """ + + > + + """, // + """ + + + """); + assertCompletion("bean-package", // + """ + + > + + """, // + """ + + + """); + + assertCompletion("value", // + """ + + /> + + """, // + """ + + + + """); + assertCompletion("override", // + """ + + /> + + """, // + """ + + + + """); + + assertCompletion("enableNs", // + """ + + + + + + """, // + """ + + + + + """); + + assertCompletion(""" + + + + """, // + """ + + + """); + assertCompletion("unknown-attr", // + """ + + + + """, // + """ + + + """); + + // 不带名字空间的补全 + assertCompletion(""" + + + + """, // + """ + + + """); + + assertCompletion("name", // + """ + + + + """, // + """ + + + """); + + // XPL 属性补全 + assertCompletion(""" + + + + + + """, // + """ + + + + + """); + } + + public void testAttributeValueCompletion() { + + } + + /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */ + protected void assertCompletion(String text, String expectedText) { + configureByXLangText(text); + myFixture.completeBasic(); + + myFixture.checkResult(expectedText); + } + + protected void assertCompletion(String selectedItem, String text, String expectedText) { + configureByXLangText(text); + myFixture.completeBasic(); + + doAssertCompletion(selectedItem, expectedText); + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java index 26b65dcb0..b83363c42 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java @@ -41,6 +41,6 @@ public class TestXLangParser extends BaseXLangPluginTestCase { protected void assertASTTree(String code, String expectedAstFile) { PsiFile testFile = configureByXLangText(code); - assertASTTree(testFile, expectedAstFile); + doAssertASTTree(testFile, expectedAstFile); } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java index b4796d6fb..eb378db14 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java @@ -39,7 +39,7 @@ public class TestXLangScriptCompletions extends BaseXLangPluginTestCase { assertFalse(items.isEmpty()); String expected = "import " + sample.replaceAll("\\.[^.]+$", ".") + items.get(0) + ";"; - assertCompletion(expected); + doAssertCompletion(expected); } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java index e8c4e25b1..ce761bc40 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -68,6 +68,6 @@ public class TestXLangScriptParser extends BaseXLangPluginTestCase { protected void assertASTTree(String code, String expectedAstFile) { PsiFile testFile = myFixture.configureByText("sample." + ext, code); - assertASTTree(testFile, expectedAstFile); + doAssertASTTree(testFile, expectedAstFile); } } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index 0ee7a07d9..76ea12fc0 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -1,6 +1,7 @@ - - - + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc index b8566f01e..b075ebe82 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc @@ -7,7 +7,7 @@ /nop/schema/xdsl.xdef - + -- Gitee From d135b1c60e280422a66d1fcacb727aacb7463e91 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 22 Jul 2025 14:44:52 +0800 Subject: [PATCH 72/82] =?UTF-8?q?nop-idea-pugin:=20=E5=A7=8B=E7=BB=88?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=8E=B7=E5=8F=96=20*.xdef=20=E7=9A=84?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=AF=B9=E8=B1=A1=E5=AE=9E=E4=BE=8B=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=20*.xdef=20=E5=8F=98=E6=9B=B4=E5=90=8E?= =?UTF-8?q?=E4=B8=8D=E8=83=BD=E8=87=AA=E5=8A=A8=E5=BE=97=E5=88=B0=E6=9C=80?= =?UTF-8?q?=E6=96=B0=E7=9A=84=20XLang=20=E8=8A=82=E7=82=B9=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/XLangScriptLanguageInjector.java | 6 +- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 472 ++++++++++++------ .../idea/plugin/lang/TestXLangReferences.java | 8 +- .../test/resources/_vfs/test/doc/example.xdef | 4 +- .../_vfs/test/java/XDefBodyType.java | 30 ++ 5 files changed, 358 insertions(+), 162 deletions(-) create mode 100644 nop-idea-plugin/src/test/resources/_vfs/test/java/XDefBodyType.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java index 2fe9034bd..0fc15d2e9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java @@ -39,8 +39,10 @@ public class XLangScriptLanguageInjector implements LanguageInjector { // 针对仅包含文本内容的 Xpl 类型节点(xdef:value=xpl*) if (!(host instanceof XLangText) // || !(host.getParent() instanceof XLangTag tag) // - //|| !tag.isXplDefNode() // - || !"c:script".equals(tag.getName()) // TODO 暂时仅针对 c:script 标签做内嵌代码解析 + || !tag.isXplDefNode() // + || (!"c:script".equals(tag.getName()) // + && !tag.isXlibSourceNode() // TODO 暂时仅针对 c:script/source 标签做内嵌代码解析 + ) // || tag.hasChildTag() // ) { return; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 30ae36d47..b0237a58e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -10,6 +10,7 @@ package io.nop.idea.plugin.lang.psi; import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; @@ -44,6 +45,7 @@ import io.nop.xlang.xdsl.XDslConstants; import io.nop.xlang.xdsl.XDslKeys; import io.nop.xlang.xpl.xlib.XlibConstants; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import static com.intellij.psi.xml.XmlElementType.XML_TAG; import static com.intellij.psi.xml.XmlElementType.XML_TEXT; @@ -57,6 +59,8 @@ import static com.intellij.psi.xml.XmlElementType.XML_TEXT; * @date 2025-07-09 */ public class XLangTag extends XmlTagImpl { + private static final SchemaMeta UNKNOWN_SCHEMA_META = new UnknownSchemaMeta(); + private static final XDefTypeDecl STD_DOMAIN_XDEF_REF = new XDefTypeDeclParser().parseFromText(null, XDefConstants.STD_DOMAIN_XDEF_REF); @@ -142,22 +146,14 @@ public class XLangTag extends XmlTagImpl { return refs.toArray(PsiReference.EMPTY_ARRAY); } - /** - * 获取当前标签所在的元模型 - * - * @see SchemaMeta#schemaDef - */ + /** @see SchemaMeta#getSchemaDef() */ private IXDefinition getSchemaDef() { - return getSchemaMeta().schemaDef; + return getSchemaMeta().getSchemaDef(); } - /** - * 获取当前标签在{@link #getSchemaDef() 元模型}中对应的节点 - * - * @see SchemaMeta#schemaDefNode - */ + /** @see SchemaMeta#getSchemaDefNode() */ public IXDefNode getSchemaDefNode() { - return getSchemaMeta().schemaDefNode; + return getSchemaMeta().getSchemaDefNode(); } /** @@ -191,13 +187,9 @@ public class XLangTag extends XmlTagImpl { return defNode != null ? defNode.getXdefValue() : null; } - /** - * 获取当前标签在 xdsl.xdef 中对应的节点 - * - * @see SchemaMeta#xdslDefNode - */ + /** @see SchemaMeta#getXDslDefNode() */ public IXDefNode getXDslDefNode() { - return getSchemaMeta().xdslDefNode; + return getSchemaMeta().getXDslDefNode(); } /** 获取当前标签上指定属性在 xdsl.xdef 中的定义 */ @@ -208,44 +200,34 @@ public class XLangTag extends XmlTagImpl { return getXDefNodeAttr(getXDslDefNode(), attrName); } - /** @see SchemaMeta#selfDef */ - public IXDefinition getSelfDef() { - return getSchemaMeta().selfDef; - } - - /** @see SchemaMeta#selfDefNode */ + /** @see SchemaMeta#getSelfDefNode() */ public IXDefNode getSelfDefNode() { - return getSchemaMeta().selfDefNode; + return getSchemaMeta().getSelfDefNode(); } - /** @see SchemaMeta#xdefKeys */ + /** @see SchemaMeta#getXDefKeys() */ public XDefKeys getXDefKeys() { - return getSchemaMeta().xdefKeys; + return getSchemaMeta().getXDefKeys(); } - /** @see SchemaMeta#xdslKeys */ + /** @see SchemaMeta#getXDslKeys() */ public XDslKeys getXDslKeys() { - return getSchemaMeta().xdslKeys; + return getSchemaMeta().getXDslKeys(); } - /** @see SchemaMeta#_inXDefXDef */ + /** @see SchemaMeta#isInXDefXDef() */ private boolean isInXDefXDef() { - return getSchemaMeta()._inXDefXDef; - } - - /** @see SchemaMeta#_inXDslXDef */ - private boolean isInXDslXDef() { - return getSchemaMeta()._inXDslXDef; + return getSchemaMeta().isInXDefXDef(); } - /** @see SchemaMeta#_xplDefNode */ + /** @see SchemaMeta#isXplDefNode() */ public boolean isXplDefNode() { - return getSchemaMeta()._xplDefNode; + return getSchemaMeta().isXplDefNode(); } - /** @see SchemaMeta#_xlibSourceNode */ + /** @see SchemaMeta#isXlibSourceNode() */ public boolean isXlibSourceNode() { - return getSchemaMeta()._xlibSourceNode; + return getSchemaMeta().isXlibSourceNode(); } /** 当前标签是否允许拥有子标签 */ @@ -442,6 +424,10 @@ public class XLangTag extends XmlTagImpl { return null; } + private static IXDefNode getXDefNodeChild(IXDefNode xdefNode, String tagName) { + return xdefNode != null ? xdefNode.getChild(tagName) : null; + } + public static String changeNamespace(String name, String fromNs, String toNs) { if (fromNs == null || toNs == null || fromNs.equals(toNs)) { return name; @@ -465,7 +451,7 @@ public class XLangTag extends XmlTagImpl { schemaMeta = null; // Note: 避免后续访问成员变量出现 NPE 问题 - return SchemaMeta.UNKNOWN; + return UNKNOWN_SCHEMA_META; } } return schemaMeta; @@ -477,167 +463,345 @@ public class XLangTag extends XmlTagImpl { return createSchemaMetaForRootTag(this); } + Project project = getProject(); String tagNs = getNamespacePrefix(); String tagName = getName(); + SchemaMeta parentSchemaMeta = parentTag.getSchemaMeta(); + + return new ChildTagSchemaMeta(project, parentSchemaMeta, tagName, tagNs); + } + + private static SchemaMeta createSchemaMetaForRootTag(XLangTag rootTag) { + String schemaUrl = XDefPsiHelper.getSchemaPath(rootTag); + if (schemaUrl == null) { + return UNKNOWN_SCHEMA_META; + } - // Note: 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 - IXDefinition schemaDef = parentTag.getSchemaDef(); - IXDefinition selfDef = parentTag.getSelfDef(); + String xdefNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDEF); + String xdslNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); + XDefKeys xdefKeys = XDefKeys.of(xdefNs); + XDslKeys xdslKeys = XDslKeys.of(xdslNs); - XDefKeys xdefKeys = parentTag.getXDefKeys(); - XDslKeys xdslKeys = parentTag.getXDslKeys(); - IXDefNode parentSchemaDefNode = parentTag.getSchemaDefNode(); - IXDefNode parentXDslDefNode = parentTag.getXDslDefNode(); - IXDefNode parentSelfDefNode = parentTag.getSelfDefNode(); + Supplier selfDef = () -> null; + // x:schema 为 /nop/schema/xdef.xdef 时,其自身也为元模型 + if (XDslConstants.XDSL_SCHEMA_XDEF.equals(schemaUrl)) { + String vfsPath = XmlPsiHelper.getNopVfsPath(rootTag); - if (parentTag.isXplDefNode()) { - // Xpl 子节点均为 xdef:unknown-tag - parentSchemaDefNode = XDefPsiHelper.getXplDef().getRootNode().getXdefUnknownTag(); + if (vfsPath != null) { + selfDef = () -> XDefPsiHelper.loadSchema(vfsPath); + } else { + // 适配单元测试环境:待测试资源可能不是标准的 vfs 资源 + IXDefinition def = XDefPsiHelper.loadSchema(rootTag.getContainingFile()); + selfDef = () -> def; + } } - IXDefNode xdslDefNode = parentXDslDefNode != null ? parentXDslDefNode.getChild(tagName) : null; - IXDefNode selfDefNode = parentSelfDefNode != null ? parentSelfDefNode.getChild(tagName) : null; + Project project = rootTag.getProject(); - IXDefNode schemaDefNode = null; - if (XDslKeys.DEFAULT.NS.equals(tagNs) && !parentTag.isInXDslXDef()) { - schemaDefNode = xdslDefNode; - selfDefNode = null; - } // - else if (parentSchemaDefNode != null) { + return new RootTagSchemaMeta(project, schemaUrl, xdefKeys, xdslKeys, selfDef); + } + + private static class RootTagSchemaMeta extends SchemaMeta { + protected final String schemaUrl; + protected final XDefKeys xdefKeys; + protected final XDslKeys xdslKeys; + + private final Supplier selfDef; + + RootTagSchemaMeta( + Project project, String schemaUrl, // + XDefKeys xdefKeys, XDslKeys xdslKeys, // + Supplier selfDef + ) { + super(project, null); + this.schemaUrl = schemaUrl; + this.xdefKeys = xdefKeys; + this.xdslKeys = xdslKeys; + this.selfDef = selfDef; + } + + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + @Override + public IXDefinition getSchemaDef() { + return ProjectEnv.withProject(project, () -> XDefPsiHelper.loadSchema(schemaUrl)); + } + + @Override + public IXDefNode getSchemaDefNode() { + IXDefinition def = getSchemaDef(); + + return def != null ? def.getRootNode() : null; + } + + @Override + public @Nullable IXDefNode getXDslDefNode() { + return ProjectEnv.withProject(project, () -> XDefPsiHelper.getXDslDef().getRootNode()); + } + + @Override + public @Nullable IXDefinition getSelfDef() { + return ProjectEnv.withProject(project, selfDef); + } + + @Override + public IXDefNode getSelfDefNode() { + IXDefinition def = getSelfDef(); + + return def != null ? def.getRootNode() : null; + } + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + + @Override + public @NotNull XDefKeys getXDefKeys() { + return xdefKeys; + } + + @Override + public @NotNull XDslKeys getXDslKeys() { + return xdslKeys; + } + } + + private static class ChildTagSchemaMeta extends SchemaMeta { + protected final SchemaMeta parent; + protected final String tagNs; + + ChildTagSchemaMeta(Project project, SchemaMeta parent, String tagName, String tagNs) { + super(project, tagName); + this.parent = parent; + this.tagNs = tagNs; + } + + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + @Override + public @Nullable IXDefinition getSchemaDef() { + return parent.getSchemaDef(); + } + + @Override + public @Nullable IXDefNode getSchemaDefNode() { + if (XDslKeys.DEFAULT.NS.equals(tagNs) && !parent.isInXDslXDef()) { + return getXDslDefNode(); + } + + IXDefNode parentDefNode = parent.getSchemaDefNode(); + if (parent.isXplDefNode()) { + // Xpl 子节点均为 xdef:unknown-tag + parentDefNode = ProjectEnv.withProject(project, + () -> XDefPsiHelper.getXplDef() + .getRootNode() + .getXdefUnknownTag()); + } + + if (parentDefNode == null) { + return null; + } + + IXDefNode defNode; // 在元元模型中,以 xdef 为名字空间的标签, // 需以 meta:unknown-tag 作为其节点定义,即,交叉定义 - if (parentTag.isInXDefXDef() && XDefKeys.DEFAULT.NS.equals(tagNs)) { - schemaDefNode = parentSchemaDefNode.getXdefUnknownTag(); + if (parent.isInXDefXDef() && XDefKeys.DEFAULT.NS.equals(tagNs)) { + defNode = parentDefNode.getXdefUnknownTag(); } // 其余的,则将标签的 xdef 名字空间固定为名字 xdef else { - tagName = changeNamespace(tagName, xdefKeys.NS, XDefKeys.DEFAULT.NS); + XDefKeys xdefKeys = parent.getXDefKeys(); + String newTagName = changeNamespace(tagName, xdefKeys.NS, XDefKeys.DEFAULT.NS); - schemaDefNode = parentSchemaDefNode.getChild(tagName); + defNode = getXDefNodeChild(parentDefNode, newTagName); } + + return defNode; + } + + @Override + public @Nullable IXDefNode getXDslDefNode() { + IXDefNode parentDefNode = parent.getXDslDefNode(); + + return getXDefNodeChild(parentDefNode, tagName); + } + + @Override + public @Nullable IXDefinition getSelfDef() { + return parent.getSelfDef(); } - return new SchemaMeta(schemaDef, schemaDefNode, xdslDefNode, selfDef, selfDefNode, xdefKeys, xdslKeys, tagName); + @Override + public @Nullable IXDefNode getSelfDefNode() { + // 在非 xdsl.xdef 中的 x 名字空间的节点,始终不视为自定义节点 + if (XDslKeys.DEFAULT.NS.equals(tagNs) && !parent.isInXDslXDef()) { + return null; + } + + IXDefNode parentDefNode = parent.getSelfDefNode(); + + return getXDefNodeChild(parentDefNode, tagName); + } + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + + @Override + public @NotNull XDefKeys getXDefKeys() { + return parent.getXDefKeys(); + } + + @Override + public @NotNull XDslKeys getXDslKeys() { + return parent.getXDslKeys(); + } } - private static SchemaMeta createSchemaMetaForRootTag(XLangTag rootTag) { - String schemaUrl = XDefPsiHelper.getSchemaPath(rootTag); - if (schemaUrl == null) { - return SchemaMeta.UNKNOWN; + private static class UnknownSchemaMeta extends SchemaMeta { + + UnknownSchemaMeta() { + super(null, null); } - String rootTagName = rootTag.getName(); - // Note: 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 - IXDefinition schemaDef = XDefPsiHelper.loadSchema(schemaUrl); + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - String xdefNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDEF); - String xdslNs = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); - XDefKeys xdefKeys = XDefKeys.of(xdefNs); - XDslKeys xdslKeys = XDslKeys.of(xdslNs); + @Override + public @Nullable IXDefinition getSchemaDef() { + return null; + } - IXDefinition selfDef = null; - // x:schema 为 /nop/schema/xdef.xdef 时,其自身也为元模型 - if (XDslConstants.XDSL_SCHEMA_XDEF.equals(schemaUrl)) { - String vfsPath = XmlPsiHelper.getNopVfsPath(rootTag); + @Override + public @Nullable IXDefNode getSchemaDefNode() { + return null; + } - if (vfsPath != null) { - selfDef = XDefPsiHelper.loadSchema(vfsPath); - } else { - // 适配单元测试环境:待测试资源可能不是标准的 vfs 资源 - selfDef = XDefPsiHelper.loadSchema(rootTag.getContainingFile()); - } + @Override + public @Nullable IXDefNode getXDslDefNode() { + return null; + } + + @Override + public @Nullable IXDefinition getSelfDef() { + return null; + } + + @Override + public @Nullable IXDefNode getSelfDefNode() { + return null; + } + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + + @Override + public @NotNull XDefKeys getXDefKeys() { + return XDefKeys.DEFAULT; + } + + @Override + public @NotNull XDslKeys getXDslKeys() { + return XDslKeys.DEFAULT; + } + } + + /** + * 注意,由于涉及对 *.xdef 的修改,因此,需采用实时加载方式获取 {@link IXDefinition} + * 和 {@link IXDefNode},不能缓存其实体对象。{@link XDefPsiHelper#loadSchema(String)} + * 是有缓存和失效机制的,不会明显影响性能 + */ + private static abstract class SchemaMeta { + protected final Project project; + protected final String tagName; + + SchemaMeta(Project project, String tagName) { + this.project = project; + this.tagName = tagName; } - IXDefNode schemaDefNode = schemaDef != null ? schemaDef.getRootNode() : null; - IXDefNode xdslDefNode = XDefPsiHelper.getXDslDef().getRootNode(); - IXDefNode selfDefNode = selfDef != null ? selfDef.getRootNode() : null; - - return new SchemaMeta(schemaDef, - schemaDefNode, - xdslDefNode, - selfDef, - selfDefNode, - xdefKeys, - xdslKeys, - rootTagName); - } - - private static class SchemaMeta { - public static final SchemaMeta UNKNOWN = new SchemaMeta(null, - null, - null, - null, - null, - XDefKeys.DEFAULT, - XDslKeys.DEFAULT, - null); - - /** 当前标签所在的元模型(在 *.xdef 中定义) */ - public final IXDefinition schemaDef; - /** 当前标签在 {@link #schemaDef} 中所对应的节点 */ - public final IXDefNode schemaDefNode; + /** + * 当前标签所在的元模型(在 *.xdef 中定义) + *

+ * 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 + */ + public abstract @Nullable IXDefinition getSchemaDef(); + + /** 当前标签在 {@link #getSchemaDef()} 中所对应的节点 */ + public abstract @Nullable IXDefNode getSchemaDefNode(); + /** * 当前标签在 xdsl 模型(xdsl.xdef)中所对应的节点。 * 注:所有 DSL 模型的节点均与 xdsl.xdef 的节点存在对应 */ - public final IXDefNode xdslDefNode; + public abstract @Nullable IXDefNode getXDslDefNode(); + /** 当当前标签定义在 *.xdef 文件中时,需记录该元模型 */ - public final IXDefinition selfDef; - /** 当前标签定义在 {@link #selfDef} 中所对应的节点 */ - public final IXDefNode selfDefNode; + public abstract @Nullable IXDefinition getSelfDef(); + + /** 当前标签定义在 {@link #getSelfDef()} 中所对应的节点 */ + public abstract @Nullable IXDefNode getSelfDefNode(); + /** * /nop/schema/xdef.xdef 对应的 {@link XDefKeys}。 * 仅在元模型中设置,如 xmlns:xdef="/nop/schema/xdef.xdef" */ - public final XDefKeys xdefKeys; + public abstract @NotNull XDefKeys getXDefKeys(); + /** * /nop/schema/xdsl.xdef 对应的 {@link XDslKeys}。 * 在 DSL 模型(含元模型)中均有设置,如 xmlns:x="/nop/schema/xdsl.xdef" */ - public final XDslKeys xdslKeys; + public abstract @NotNull XDslKeys getXDslKeys(); /** 当前标签是否在元元模型 xdef.xdef 中 */ - public final boolean _inXDefXDef; + public boolean isInXDefXDef() { + IXDefinition def = getSelfDef(); + + // Note: 在单元测试中只能基于内容做判断,而不是 vfs 路径 + return def != null // + && def.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // + && !XDefKeys.DEFAULT.equals(getXDefKeys()); + } + /** 当前标签是否在 DSL 元模型 xdsl.xdef 中 */ - public final boolean _inXDslXDef; - /** 当前标签是否对应 Xpl 节点 */ - public final boolean _xplDefNode; + public boolean isInXDslXDef() { + IXDefinition def = getSelfDef(); + + // Note: 在单元测试中只能基于内容做判断,而不是 vfs 路径 + return def != null // + && def.getXdefCheckNs().contains(XDslKeys.DEFAULT.NS) // + && !XDslKeys.DEFAULT.equals(getXDslKeys()); + } + /** 当前标签是否对应 Xlib 的 source 节点 */ - public final boolean _xlibSourceNode; + public boolean isXlibSourceNode() { + IXDefNode defNode = getSchemaDefNode(); + if (defNode == null) { + return false; + } - private SchemaMeta( - IXDefinition schemaDef, IXDefNode schemaDefNode, // - IXDefNode xdslDefNode, // - IXDefinition selfDef, IXDefNode selfDefNode, // - XDefKeys xdefKeys, XDslKeys xdslKeys, // - String tagName - ) { - this.schemaDef = schemaDef; - this.schemaDefNode = schemaDefNode; - this.xdslDefNode = xdslDefNode; - this.selfDef = selfDef; - this.selfDefNode = selfDefNode; - this.xdefKeys = xdefKeys; - this.xdslKeys = xdslKeys; + String defPath = XmlPsiHelper.getNopVfsPath(defNode); - // Note: 在单元测试中只能基于内容做判断,而不是 vfs 路径 - this._inXDefXDef = selfDef != null // - && selfDef.getXdefCheckNs().contains(XDefKeys.DEFAULT.NS) // - && !XDefKeys.DEFAULT.equals(xdefKeys); - this._inXDslXDef = selfDef != null // - && selfDef.getXdefCheckNs().contains(XDslKeys.DEFAULT.NS) // - && !XDslKeys.DEFAULT.equals(xdslKeys); - - String defPath = XmlPsiHelper.getNopVfsPath(schemaDefNode); + return isXlibSourceNode(defNode, defPath); + } + + /** 当前标签是否对应 Xpl 节点 */ + public boolean isXplDefNode() { + IXDefNode defNode = getSchemaDefNode(); + if (defNode == null) { + return false; + } + + String defPath = XmlPsiHelper.getNopVfsPath(defNode); + + if (XDefPsiHelper.isXplDefNode(defNode) // + || XDslConstants.XDSL_SCHEMA_XPL.equals(defPath) // + ) { + return true; + } + + return isXlibSourceNode(defNode, defPath); + } + + private boolean isXlibSourceNode(IXDefNode defNode, String defPath) { // xlib.xdef 中的 source 标签设置为 xml 类型,是因为在获取 XplLib 模型的时候会根据 xlib.xdef 来解析, // 但此时这个 source 段无法自动进行编译,必须结合它的 outputMode 和 attrs 配置等才能决定。 // 因此,将其子节点同样视为 xpl 节点处理 - this._xlibSourceNode = XDslConstants.XDSL_SCHEMA_XLIB.equals(defPath) - && XDefConstants.STD_DOMAIN_XML.equals(XDefPsiHelper.getDefNodeType(schemaDefNode)) - && XlibConstants.SOURCE_NAME.equals(tagName); - this._xplDefNode = this._xlibSourceNode // - || XDefPsiHelper.isXplDefNode(schemaDefNode) // - || XDslConstants.XDSL_SCHEMA_XPL.equals(defPath); + return XDslConstants.XDSL_SCHEMA_XLIB.equals(defPath) + && XDefConstants.STD_DOMAIN_XML.equals(XDefPsiHelper.getDefNodeType(defNode)) + && XlibConstants.SOURCE_NAME.equals(tagName); } } } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 343bbc7a2..54b473e12 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -894,10 +894,10 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { ); // dict/enum 类型的值引用 - assertReference(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // - "xdef:default-override=\"append\"", // - "xdef:default-override=\"append\""), // - "io.nop.xlang.xdef.XDefOverride#APPEND" // + assertReference(insertCaretIntoVfs("/nop/schema/xlib.xdef", // + "ap\""), // + "io.nop.xlang.xdef.XDefBodyType#map" // ); assertReference(insertCaretIntoVfs("/nop/schema/xlib.xdef", // "macro=\"!boolean=false\"", // diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef index 7ccc91b19..264794eed 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -20,11 +20,11 @@ @type [Child Type] This is child type @xdef:unknown-attr This a unknown attribute --> - + - + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefBodyType.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefBodyType.java new file mode 100644 index 000000000..392a786ef --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefBodyType.java @@ -0,0 +1,30 @@ +package io.nop.xlang.xdef; + +import java.util.HashMap; +import java.util.Map; + +import io.nop.api.core.annotations.core.Description; +import io.nop.api.core.annotations.core.Locale; +import io.nop.api.core.annotations.core.StaticFactoryMethod; + +@Locale("zh-CN") +public enum XDefBodyType { + @Description("解析为列表,如果指定了xdef:key-attr,则按照key检查唯一性") list, + + @Description("最多只允许一个子节点") union, + + @Description("解析为Map,子节点名作为key") map; + + private static final Map textMap = new HashMap<>(); + + static { + for (XDefBodyType value : XDefBodyType.values()) { + textMap.put(value.name(), value); + } + } + + @StaticFactoryMethod + public static XDefBodyType fromText(String text) { + return textMap.get(text); + } +} -- Gitee From b188a33c3a457cf1f9ff204013c14ede055d1f7f Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 22 Jul 2025 23:15:09 +0800 Subject: [PATCH 73/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20XLang=20Xlib=20=E6=A0=87=E7=AD=BE=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB=E5=92=8C=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E8=A1=A5=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/XLangScriptLanguageInjector.java | 3 +- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 165 +++++++++++++++++- .../lang/reference/XLangTagReference.java | 18 +- .../reference/XLangXlibTagNsReference.java | 39 +++++ .../lang/reference/XLangXlibTagReference.java | 100 +++++++++++ .../reference/XLangReferenceProvider.java | 87 --------- .../idea/plugin/services/NopAppListener.java | 5 + .../plugin/services/NopProjectService.java | 14 ++ .../plugin/utils/LookupElementHelper.java | 14 ++ .../nop/idea/plugin/utils/XmlPsiHelper.java | 86 --------- .../messages/NopPluginBundle.properties | 1 + .../messages/NopPluginBundle_zh.properties | 1 + .../plugin/lang/TestXLangCompletions.java | 37 ++++ .../idea/plugin/lang/TestXLangReferences.java | 135 ++++++++------ .../test/resources/_vfs/test/reference/a.xlib | 6 + 15 files changed, 459 insertions(+), 252 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagNsReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java delete mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java index 0fc15d2e9..c94e4b933 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java @@ -17,6 +17,7 @@ import io.nop.commons.util.StringHelper; import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.lang.psi.XLangText; import io.nop.idea.plugin.lang.script.XLangScriptLanguage; +import io.nop.xlang.xpl.XplConstants; import org.jetbrains.annotations.NotNull; /** @@ -40,7 +41,7 @@ public class XLangScriptLanguageInjector implements LanguageInjector { if (!(host instanceof XLangText) // || !(host.getParent() instanceof XLangTag tag) // || !tag.isXplDefNode() // - || (!"c:script".equals(tag.getName()) // + || (!XplConstants.TAG_C_SCRIPT.equals(tag.getName()) // && !tag.isXlibSourceNode() // TODO 暂时仅针对 c:script/source 标签做内嵌代码解析 ) // || tag.hasChildTag() // diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index b0237a58e..4380f8204 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -16,10 +16,13 @@ import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.TagNameReference; import com.intellij.psi.impl.source.xml.XmlTagImpl; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlToken; import com.intellij.xml.util.XmlTagUtil; import io.nop.api.core.util.SourceLocation; @@ -27,6 +30,8 @@ import io.nop.commons.util.StringHelper; import io.nop.core.lang.xml.XNode; import io.nop.idea.plugin.lang.XLangDocumentation; import io.nop.idea.plugin.lang.reference.XLangTagReference; +import io.nop.idea.plugin.lang.reference.XLangXlibTagNsReference; +import io.nop.idea.plugin.lang.reference.XLangXlibTagReference; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -43,7 +48,9 @@ import io.nop.xlang.xdef.impl.XDefAttribute; import io.nop.xlang.xdef.parse.XDefTypeDeclParser; import io.nop.xlang.xdsl.XDslConstants; import io.nop.xlang.xdsl.XDslKeys; +import io.nop.xlang.xpl.XplConstants; import io.nop.xlang.xpl.xlib.XlibConstants; +import io.nop.xlang.xpl.xlib.XplLibHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -111,16 +118,26 @@ public class XLangTag extends XmlTagImpl { public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { List refs = new ArrayList<>(); - // TODO xlib 函数标签的名字空间引用的是 xlib 的文件名字 + XlibTagMeta xlibTag = getXlibTagMeta(); + // 参考 XmlTagDelegate#getReferencesImpl PsiReference[] xmlRefs = super.getReferences(hints); // Note: 仅保留对名字空间的引用,以支持对其做高亮、重命名等 for (PsiReference ref : xmlRefs) { - if (!(ref instanceof TagNameReference)) { + // Note: xlib 函数标签的名字空间引用的是 xlib 的文件名字 + if (!(ref instanceof TagNameReference) && xlibTag == null) { refs.add(ref); } } + if (xlibTag != null) { + XmlToken startTagName = XmlTagUtil.getStartTagNameElement(this); + TextRange textRange = TextRange.allOf(xlibTag.tagNs).shiftRight(startTagName.getStartOffsetInParent()); + + PsiReference ref = new XLangXlibTagNsReference(this, textRange, xlibTag.tagNs, xlibTag.ref); + refs.add(ref); + } + // 对起止标签均做引用识别 XmlToken[] tagNameTokens = new XmlToken[] { XmlTagUtil.getStartTagNameElement(this), // @@ -136,9 +153,13 @@ public class XLangTag extends XmlTagImpl { // Note: 针对起止标签名在当前标签中的文本范围创建引用,而不是针对起止标签名自身创建引用 TextRange textRange = TextRange.allOf(name.substring(nsIndex + 1)) .shiftRight(token.getStartOffsetInParent() + nsIndex + 1); - XLangTagReference ref = new XLangTagReference(this, textRange); - // TODO 对 xlib 标签单独做引用识别 + PsiReference ref; + if (xlibTag == null) { + ref = new XLangTagReference(this, textRange); + } else { + ref = new XLangXlibTagReference(this, textRange, xlibTag.tagName, xlibTag.xlibPath); + } refs.add(ref); } @@ -174,6 +195,8 @@ public class XLangTag extends XmlTagImpl { attrName = changeNamespace(attrName, getXDefKeys().NS, XDefKeys.DEFAULT.NS); } + // TODO 为 xlib 标签函数的参数构造定义 + return getXDefNodeAttr(getSchemaDefNode(), attrName); } @@ -269,6 +292,66 @@ public class XLangTag extends XmlTagImpl { || !def.getXdefCheckNs().contains(ns); } + /** 若当前标签对应的是 xlib 的函数节点,则返回该函数节点信息 */ + protected XlibTagMeta getXlibTagMeta() { + String tagNs = getNamespacePrefix(); + if (StringHelper.isEmpty(tagNs)) { + return null; + } + + XLangTag parentTag = getParentTag(); + if (parentTag == null || !parentTag.isXplDefNode()) { + return null; + } + + String lib; + XmlElement ref = null; + boolean selfImported = false; + if (XplConstants.XPL_THIS_LIB_NS.equals(tagNs)) { + // Note: 单元测试内,可能得不到当前标签所在文件的 vfs 路径 + lib = XmlPsiHelper.getNopVfsPath(this); + ref = this; + + // 支持在路径形式为 /xlib/{libName}/impl_xxx.xpl 的 xpl 文件中引用 {libName} 中的标签函数。 + // 如,在 /nop/web/xlib/web/page_crud.xpl 中可引用 /nop/web/xlib/web.xlib 中的标签函数 + if (lib != null && !lib.endsWith(XplConstants.POSTFIX_XLIB)) { + int pos = lib.lastIndexOf("/xlib/"); + if (pos > 0) { + pos += "/xlib/".length(); + + int pos2 = lib.indexOf('/', pos); + if (pos2 > 0) { + lib = lib.substring(0, pos2) + XplConstants.POSTFIX_XLIB; + } + } + } + } else { + XmlAttribute libAttr = getAttribute(XplConstants.ATTR_XPL_LIB); + + lib = libAttr != null ? libAttr.getValue() : null; + + selfImported = lib != null; + if (!selfImported) { + XLangTag importTag = XlibTagMeta.findXlibImportTag(this, tagNs); + + if (importTag != null) { + lib = importTag.getAttribute(XplConstants.FROM_NAME).getValue(); + ref = importTag; + } + } else { + ref = libAttr; + } + } + + if (ref != this && (lib == null || !lib.endsWith(XplConstants.POSTFIX_XLIB))) { + return null; + } + + String tagName = getLocalName(); + + return new XlibTagMeta(ref, tagNs, tagName, lib, selfImported); + } + /** 获取当前标签的说明文档 */ public XLangDocumentation getTagDocumentation() { String tagNs = getNamespacePrefix(); @@ -298,6 +381,8 @@ public class XLangTag extends XmlTagImpl { defNode = getSchemaDefNode(); } + // TODO 为 xlib 标签函数构造文档 + if (defNode == null) { return null; } @@ -345,6 +430,8 @@ public class XLangTag extends XmlTagImpl { return null; } + // TODO 为 xlib 标签函数的参数构造文档 + XLangDocumentation doc = new XLangDocumentation(defAttr); doc.setMainTitle(mainTitle); @@ -463,9 +550,10 @@ public class XLangTag extends XmlTagImpl { return createSchemaMetaForRootTag(this); } - Project project = getProject(); - String tagNs = getNamespacePrefix(); String tagName = getName(); + String tagNs = getNamespacePrefix(); + + Project project = getProject(); SchemaMeta parentSchemaMeta = parentTag.getSchemaMeta(); return new ChildTagSchemaMeta(project, parentSchemaMeta, tagName, tagNs); @@ -501,6 +589,71 @@ public class XLangTag extends XmlTagImpl { return new RootTagSchemaMeta(project, schemaUrl, xdefKeys, xdslKeys, selfDef); } + protected static class XlibTagMeta { + private final XmlElement ref; + + /** xlib 标签函数的名字空间 */ + public final String tagNs; + /** xlib 标签函数的名字:不含名字空间 */ + public final String tagName; + + /** xlib 的 vfs 路径 */ + public final String xlibPath; + /** 是否在节点上通过 {@link XplConstants#ATTR_XPL_LIB} 属性导入 */ + private final boolean selfImported; + + XlibTagMeta(@NotNull XmlElement ref, String tagNs, String tagName, String xlibPath, boolean selfImported) { + this.ref = ref; + + this.tagNs = tagNs; + this.tagName = tagName; + + this.xlibPath = xlibPath; + this.selfImported = selfImported; + } + + private static String getXlibAlias(String path) { + try { + return XplLibHelper.getNamespaceFromLibPath(path); + } catch (Exception ignore) { + return null; + } + } + + public static XLangTag findXlibImportTag(XLangTag tag, String alias) { + XLangTag parentTag = tag != null ? tag.getParentTag() : null; + if (parentTag == null || !parentTag.isXplDefNode()) { + return null; + } + + for (PsiElement child : parentTag.getChildren()) { + if (!(child instanceof XLangTag childTag) // + || !XplConstants.TAG_C_IMPORT.equals(childTag.getName()) // + ) { + continue; + } + + XmlAttribute fromAttr = childTag.getAttribute(XplConstants.FROM_NAME); + String from = fromAttr != null ? fromAttr.getValue() : null; + if (from == null) { + continue; + } + + XmlAttribute asAttr = childTag.getAttribute(XplConstants.AS_NAME); + String as = asAttr != null ? asAttr.getValue() : null; + if (as == null) { + as = getXlibAlias(from); + } + + if (alias.equals(as)) { + return childTag; + } + } + + return findXlibImportTag(parentTag, alias); + } + } + private static class RootTagSchemaMeta extends SchemaMeta { protected final String schemaUrl; protected final XDefKeys xdefKeys; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java index 405505ede..0f6b20aed 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java @@ -13,15 +13,13 @@ import java.util.List; import java.util.Set; import java.util.function.Function; -import com.intellij.codeInsight.completion.XmlTagInsertHandler; import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; -import com.intellij.util.PlatformIcons; import io.nop.api.core.util.SourceLocation; import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.utils.LookupElementHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xdef.IXDefComment; @@ -92,7 +90,8 @@ public class XLangTagReference extends XLangReferenceBase { } return result.stream() - .sorted((a, b) -> XLangReferenceHelper.XLANG_NAME_COMPARATOR.compare(a.getTagName(), b.getTagName())) + .sorted((a, b) -> XLangReferenceHelper.XLANG_NAME_COMPARATOR.compare(a.getTagName(), + b.getTagName())) .map((defNode) -> lookupTag(defNode, !tagNs.isEmpty())) .toArray(LookupElement[]::new); } @@ -130,15 +129,6 @@ public class XLangTagReference extends XLangReferenceBase { tagName = tagName.substring(tagName.indexOf(':') + 1); } - return LookupElementBuilder.create(tagName) - // icon 靠左布局 - .withIcon(PlatformIcons.XML_TAG_ICON) - // type text 靠后布局 - .withTypeText(label) -// // tail text 与 lookup string 紧挨着 -// .withTailText(label) // -// // presentable text 将替换 lookup string 作为最终的显示文本 -// .withPresentableText(label) // - .withInsertHandler(new XmlTagInsertHandler()); + return LookupElementHelper.lookupXmlTag(tagName, label); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagNsReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagNsReference.java new file mode 100644 index 000000000..53857c523 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagNsReference.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.psi.XLangTag; +import org.jetbrains.annotations.Nullable; + +/** + * 对 xlib 函数标签的名字空间的引用识别 + * + * @author flytreeleft + * @date 2025-07-22 + */ +public class XLangXlibTagNsReference extends XLangReferenceBase { + private final String tagNs; + private final XmlElement target; + + public XLangXlibTagNsReference( + XLangTag myElement, TextRange myRangeInElement, String tagNs, XmlElement target + ) { + super(myElement, myRangeInElement); + this.tagNs = tagNs; + this.target = target; + } + + @Override + public @Nullable PsiElement resolveInner() { + return target; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java new file mode 100644 index 000000000..e2f489da0 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang.reference; + +import java.util.function.Function; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.idea.plugin.utils.LookupElementHelper; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import io.nop.xlang.xpl.IXplTag; +import io.nop.xlang.xpl.IXplTagLib; +import io.nop.xlang.xpl.xlib.XplLibHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对 xlib 函数标签的引用识别:指向函数的定义位置 + * + * @author flytreeleft + * @date 2025-07-22 + */ +public class XLangXlibTagReference extends XLangReferenceBase { + /** xlib 标签函数名:不含名字空间 */ + private final String tagName; + /** xlib 的 vfs 路径 */ + private final String xlibPath; + + public XLangXlibTagReference(XLangTag myElement, TextRange myRangeInElement, String tagName, String xlibPath) { + super(myElement, myRangeInElement); + this.tagName = tagName; + this.xlibPath = xlibPath; + } + + @Override + public @Nullable PsiElement resolveInner() { + Function targetResolver = (file) -> XmlPsiHelper.findFirstElement(file, (element) -> { + if (element instanceof XLangTag tag) { + return tag.getName().equals(tagName); + } + return false; + }); + + // Note: 仅针对单元测试中的非 vfs 资源内的自引用 + if (xlibPath == null) { + return targetResolver.apply(myElement.getContainingFile()); + } + + // 得到标签函数实际定义所在的 xlib + IXplTagLib xlib = loadXlib(); + IXplTag xlibTag = xlib != null ? xlib.getTag(tagName) : null; + String targetXlibPath = XmlPsiHelper.getNopVfsPath(xlibTag); + + NopVirtualFile target = targetXlibPath != null + ? new NopVirtualFile(myElement, targetXlibPath, targetResolver) + : null; + if (target == null || target.hasEmptyChildren()) { + String msg = NopPluginBundle.message("xlang.annotation.reference.xlib-tag-not-found", tagName, xlibPath); + setUnresolvedMessage(msg); + + return null; + } + return target; + } + + @Override + public Object @NotNull [] getVariants() { + IXplTagLib xlib = loadXlib(); + if (xlib == null) { + return LookupElement.EMPTY_ARRAY; + } + + // Note: 标签函数允许递归引用,不需要排除当前标签 + return xlib.getTags().keySet().stream() // + .sorted() // + .map((name) -> LookupElementHelper.lookupXmlTag(name, null)) // + .toArray(); + } + + private IXplTagLib loadXlib() { + // xlib 是可扩展的,因此,需要直接加载 xlib 模型以获取准确的标签函数名 + // TODO 在插件内加载 DSL,是否会因为执行 x:gen-extends 等脚本而产生安全风险? + Project project = myElement.getProject(); + + return ProjectEnv.withProject(project, () -> XplLibHelper.loadLib(xlibPath)); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java deleted file mode 100644 index 0c7322457..000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/reference/XLangReferenceProvider.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.nop.idea.plugin.reference; - -import java.util.Arrays; - -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReference; -import com.intellij.psi.PsiReferenceProvider; -import com.intellij.psi.xml.XmlTag; -import com.intellij.util.ProcessingContext; -import io.nop.idea.plugin.resource.ProjectEnv; -import io.nop.idea.plugin.utils.XmlPsiHelper; -import org.jetbrains.annotations.NotNull; - -/** - * 针对 XLang 中的 {@link PsiElement 元素} 创建引用 - * - * @author flytreeleft - * @date 2025-06-22 - * @deprecated - */ -@Deprecated -public class XLangReferenceProvider extends PsiReferenceProvider { - - /** - * @param element - * 在探测的元素,若针对该元素返回了非空结果,则将该引用与 element 建立关联。 - * 因此,需要精确针对某个 element,而不能包含多余内容,从而确保在 UI 层面, - * 高亮的或可点击的部分只是该 element,而不会包含引号等无关内容 - */ - @Override - public PsiReference @NotNull [] getReferencesByElement( - @NotNull PsiElement element, @NotNull ProcessingContext context - ) { - Project project = element.getProject(); - - return ProjectEnv.withProject(project, () -> { - /* XmlTag:xdef:unknown-tag(505,3305) - XmlToken:XML_START_TAG_START('<')(505,506) - XmlToken:XML_NAME('xdef:unknown-tag')(506,522) - PsiElement(XML_ATTRIBUTE)(523,558) - XmlToken:XML_NAME('xdsl:schema')(523,534) - XmlToken:XML_EQ('=')(534,535) - PsiElement(XML_ATTRIBUTE_VALUE)(535,558) - XmlToken:XML_ATTRIBUTE_VALUE_START_DELIMITER('"')(535,536) - XmlToken:XML_ATTRIBUTE_VALUE_TOKEN('/nop/schema/xdef.xdef')(536,557) - XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"')(557,558) - */ - if (element instanceof XmlTag tag) { - return getReferencesFromXmlTag(tag); - } // - - return PsiReference.EMPTY_ARRAY; - }); - } - - /** 获取 xml 标签对应的引用(节点定义、xpl 函数定义等) */ - private PsiReference @NotNull [] getReferencesFromXmlTag(XmlTag tag) { - // TODO xpl 函数的引用:根据导入 xlib 中定义的函数进行识别 - // TODO 引用节点定义 - String tagName = tag.getName(); - - int pos = tagName.indexOf(':'); - if (pos <= 0) { - return PsiReference.EMPTY_ARRAY; - } - - // 内置的名字空间 - String ns = tagName.substring(0, pos); - - if (ns.equals("x") || ns.equals("xdef") || ns.equals("xdsl") || ns.equals("xpl") // - || ns.equals("c") || ns.equals("macro") || ns.equals("xmlns") // - ) { - return PsiReference.EMPTY_ARRAY; - } - - // Note: 仅对名字做引用识别,忽略名字空间 - TextRange textRange = new TextRange(pos + 1, tagName.length()).shiftRight(1); - - return Arrays.stream(XmlPsiHelper.findXplTag(tag.getProject(), tag)) - //.map((xpl) -> new XLangElementReference(tag, textRange, xpl)) - .toArray(PsiReference[]::new); - } -} - - diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java index 7c7a197cb..d429488eb 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java @@ -11,6 +11,7 @@ import com.intellij.ide.AppLifecycleListener; import io.nop.api.core.ApiConfigs; import io.nop.api.core.config.AppConfig; import io.nop.api.core.json.JSON; +import io.nop.commons.util.ClassHelper; import io.nop.core.dict.DictProvider; import io.nop.core.lang.json.JsonTool; import io.nop.core.resource.VirtualFileSystem; @@ -26,6 +27,10 @@ public class NopAppListener implements AppLifecycleListener { @Override public void appFrameCreated(@NotNull List commandLineArgs) { + // Note: 采用默认的加载器(ClassHelper#getDefaultClassLoader),在项目中会无法加载 IXplTagLib 的实现,原因未知 + // TODO 在加载 DSL 时,是否会因为执行 x:gen-extends 等脚本而产生安全风险? + ClassHelper.registerSafeClassLoader((name) -> getClass().getClassLoader().loadClass(name)); + AppConfig.getConfigProvider().updateConfigValue(ApiConfigs.CFG_DEBUG, false); JSON.registerProvider(JsonTool.instance()); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopProjectService.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopProjectService.java index 87e3d940b..e5fb2f64e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopProjectService.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopProjectService.java @@ -14,10 +14,13 @@ import io.nop.api.core.beans.DictBean; import io.nop.commons.lang.impl.Cancellable; import io.nop.core.dict.DictModel; import io.nop.core.resource.cache.ResourceLoadingCache; +import io.nop.core.resource.component.ComponentModelConfig; import io.nop.core.resource.component.ResourceComponentManager; import io.nop.idea.plugin.resource.ProjectDictProvider; import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.xlang.XLangConstants; import io.nop.xlang.initialize.XLangCoreInitializer; +import io.nop.xlang.xdsl.DslModelParser; @Service public final class NopProjectService implements Disposable { @@ -46,6 +49,8 @@ public final class NopProjectService implements Disposable { XLangCoreInitializer xlang = new XLangCoreInitializer(); xlang.initialize(); cleanup.appendOnCancelTask(xlang::destroy); + + registerXlib(); return null; }); } @@ -68,4 +73,13 @@ public final class NopProjectService implements Disposable { public void dispose() { cleanup.cancel(); } + + private void registerXlib() { + ComponentModelConfig config = new ComponentModelConfig(); + config.modelType(XLangConstants.MODEL_TYPE_XLIB); + config.loader(XLangConstants.FILE_TYPE_XLIB, + path -> new DslModelParser(XLangConstants.XDSL_SCHEMA_XLIB).parseFromVirtualPath(path)); + + cleanup.append(ResourceComponentManager.instance().registerComponentModelConfig(config)); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java index 0b094450b..3db8a015f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java @@ -12,6 +12,7 @@ import java.util.Comparator; import java.util.function.Function; import com.intellij.codeInsight.completion.JavaClassNameCompletionContributor; +import com.intellij.codeInsight.completion.XmlTagInsertHandler; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.util.Iconable; @@ -29,6 +30,19 @@ import one.util.streamex.StreamEx; */ public class LookupElementHelper { + public static LookupElement lookupXmlTag(String tagName, String label) { + return LookupElementBuilder.create(tagName) + // icon 靠左布局 + .withIcon(PlatformIcons.XML_TAG_ICON) + // type text 靠后布局 + .withTypeText(label) +// // tail text 与 lookup string 紧挨着 +// .withTailText(label) // +// // presentable text 将替换 lookup string 作为最终的显示文本 +// .withPresentableText(label) // + .withInsertHandler(new XmlTagInsertHandler()); + } + public static LookupElement lookupDictOpt(DictOptionBean opt) { String optValue = ConvertHelper.toString(opt.getValue(), (String) null); String label = opt.getLabel(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 8b89f61b9..6af141f5b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -8,7 +8,6 @@ package io.nop.idea.plugin.utils; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -30,14 +29,12 @@ import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; -import com.intellij.psi.xml.XmlElementType; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlTokenType; import io.nop.api.core.util.ISourceLocationGetter; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; import io.nop.core.resource.ResourceHelper; -import io.nop.xlang.xpl.xlib.XplLibHelper; import org.jetbrains.annotations.NotNull; public class XmlPsiHelper { @@ -105,89 +102,6 @@ public class XmlPsiHelper { return XmlPsiHelper.findPsiFileList(project, absPath); } - public static List findXplLib(Project project, XmlTag tag) { - String ns = StringHelper.getNamespace(tag.getName()); - if ("thisLib".equals(ns)) { - PsiFile file = tag.getContainingFile(); - String fileName = file.getName(); - if (fileName.endsWith(".xlib")) { - // 同一个库文件可能存在多个定制文件 - String path = ProjectFileHelper.getNopVfsPath(file.getVirtualFile()); - List list = findPsiFileList(project, path); - if (list.isEmpty()) { - list = Collections.singletonList(file); - } - return list; - } - - String path = ProjectFileHelper.getNopVfsPath(file.getVirtualFile()); - int pos = path.lastIndexOf("/xlib/"); - if (pos > 0) { - // 标签的实现文件,假定格式为/xlib/{libName}/impl_xxx.xpl - pos += "/xlib/".length(); - int pos2 = path.indexOf('/', pos); - if (pos2 > 0) { - return findPsiFileList(project, path.substring(0, pos2) + ".xlib"); - } - } - return Collections.emptyList(); - } - - XmlAttribute attr = tag.getAttribute("xpl:lib"); - if (attr != null) { - List paths = StringHelper.split(attr.getValue(), ','); - for (String path : paths) { - String libNs = XplLibHelper.getNamespaceFromLibPath(path); - if (ns.equals(libNs)) { - return findPsiFileList(project, path); - } - } - } - - PsiFile[] files = FilenameIndex.getFilesByName(project, ns + ".xlib", GlobalSearchScope.allScope(project)); - return files.length == 0 ? Collections.emptyList() : Arrays.asList(files); - } - - public static PsiElement[] findXplTag(Project project, XmlTag tag) { - List files = findXplLib(project, tag); - if (files.isEmpty()) { - return PsiElement.EMPTY_ARRAY; - } - - String tagBegin = "<" + tag.getLocalName(); - - List ret = new ArrayList<>(); - for (PsiFile file : files) { - String text = file.getText(); - int fromPos = 0; - do { - int pos = text.indexOf(tagBegin, fromPos); - if (pos >= 0) { - int end = pos + tagBegin.length(); - if (end == text.length() - || text.charAt(end) == ' ' - || text.charAt(end) == '/' - || text.charAt(end) == '>') { - PsiElement element = file.findElementAt(pos + 1); - if (element != null && isXmlTag(element)) { - ret.add(element); - break; - } - } - fromPos = end; - } else { - break; - } - } while (true); - } - return ret.toArray(PsiElement.EMPTY_ARRAY); - } - - private static boolean isXmlTag(PsiElement element) { - IElementType type = element.getNode().getElementType(); - return type == XmlElementType.XML_NAME || type == XmlElementType.XML_TAG_NAME || type == XmlElementType.XML_TAG; - } - /** 获取指定行列的 {@link PsiElement 元素} */ public static PsiElement getPsiElementAt(PsiFile psiFile, int line, int column) { Document document = PsiDocumentManager.getInstance(psiFile.getProject()).getDocument(psiFile); diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties index 446dfd721..5d0fde440 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties @@ -25,6 +25,7 @@ xlang.annotation.reference.std-domain-not-registered = Std-domain ''{0}'' isn''t xlang.annotation.reference.xdef-name-class-not-found = Java class ''{0}'' isn''t created or generated xlang.annotation.reference.enum-not-found = Java class ''{0}'' doesn''t exist xlang.annotation.reference.class-not-enum = Java class ''{0}'' isn''t Enum +xlang.annotation.reference.xlib-tag-not-found = No xlib tag <{0}/> defined in ''{1}'' xlang.completion.domain.modifier.required = Required (Attribute Modifier) xlang.completion.domain.modifier.internal = Internal (Attribute Modifier) xlang.completion.domain.modifier.allow-cp-expr = Allow ''#{var}'' (Attribute Modifier) diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties index 2b862b6ac..cf1366f2c 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties @@ -25,6 +25,7 @@ xlang.annotation.reference.std-domain-not-registered = \u6570\u636E\u57DF ''{0}' xlang.annotation.reference.xdef-name-class-not-found = \u8FD8\u672A\u521B\u5EFA\u6216\u751F\u6210 Java \u7C7B ''{0}'' xlang.annotation.reference.enum-not-found = Java \u7C7B ''{0}'' \u4E0D\u5B58\u5728 xlang.annotation.reference.class-not-enum = Java \u7C7B ''{0}'' \u4E0D\u662F\u679A\u4E3E\u7C7B +xlang.annotation.reference.xlib-tag-not-found = Xlib \u6807\u7B7E <{0}/> \u672A\u5728 ''{1}'' \u4E2D\u5B9A\u4E49 xlang.completion.domain.modifier.required = \u5FC5\u586B\uFF08\u5C5E\u6027\u4FEE\u9970\u7B26\uFF09 xlang.completion.domain.modifier.internal = \u5185\u90E8\uFF08\u5C5E\u6027\u4FEE\u9970\u7B26\uFF09 xlang.completion.domain.modifier.allow-cp-expr = \u5141\u8BB8 ''#{var}''\uFF08\u5C5E\u6027\u4FEE\u9970\u7B26\uFF09 diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java index 141a5c720..ab40c42fa 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java @@ -751,6 +751,43 @@ public class TestXLangCompletions extends BaseXLangPluginTestCase { """); } + public void testXlibTagCompletion() { + // - xpl:lib 导入库的补全 + assertCompletion("Query", // + """ + + + xpl:lib="/test/reference/a.xlib"/> + + + """, // + """ + + + + + + """); + // - c:import 导入库的补全 + assertCompletion("DoFindByMdxQuery", // + """ + + + + + + + """, // + """ + + + + + + + """); + } + /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */ protected void assertCompletion(String text, String expectedText) { configureByXLangText(text); diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 54b473e12..16a1da7cf 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -196,9 +196,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { public void testXplReferences() { // xlib 中的 source 标签内的引用 assertReference(""" - + urce> @@ -209,9 +207,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { "/nop/schema/xlib.xdef?source" // ); assertReference(""" - + @@ -226,9 +222,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { // // TODO xpl 节点内引用内置的 xpl 标签函数 // assertReference(""" -// +// // // ort from="/test/reference/a.xlib"/> // @@ -237,9 +231,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { // "" // // ); // assertReference(""" -// +// // // ipt> // @@ -248,78 +240,117 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { // "" // // ); - // TODO 通过 xpl:lib 导入 xlib + // xlib 标签函数引用识别 + // - 通过 xpl:lib 导入 xlib assertReference(""" - + ByMdxQuery xpl:lib="/test/reference/a.xlib"/> """, // - "/test/reference/a.xlib#DoFindByMdxQuery" // + "/test/reference/a.xlib?DoFindByMdxQuery" // ); + // - 通过 c:import 导入 xlib assertReference(""" - + - hod="post" xpl:lib="/test/reference/a.xlib"/> + + ByMdxQuery/> """, // - "/test/reference/a.xlib#DoFindByMdxQuery" // + "/test/reference/a.xlib?DoFindByMdxQuery" // ); - - // TODO 通过 c:import 导入 xlib assertReference(""" - + - - ByMdxQuery/> + + ByMdxQuery/> """, // - "/test/reference/a.xlib#DoFindByMdxQuery" // + "/test/reference/a.xlib?DoFindByMdxQuery" // ); + // - thisLib 函数的识别 assertReference(""" - + + + + + mething/> + + + <_DoSomething/> + + + """, // + "_DoSomething" // + ); + // - 名字空间引用识别 + assertReference(""" + - - hod="post"/> + :DoFindByMdxQuery xpl:lib="/test/reference/a.xlib"/> """, // - "/test/reference/a.xlib#DoFindByMdxQuery" // + "a:DoFindByMdxQuery#xpl:lib=/test/reference/a.xlib" // ); assertReference(""" - + - - ByMdxQuery/> + + en:DoSomething/> """, // - "/test/reference/a.xlib#DoFindByMdxQuery" // + "c:import" // ); assertReference(""" - + + + + + sLib:_DoSomething/> + + + <_DoSomething/> + + + """, // + "thisLib:_DoSomething" // + ); + + // - TODO 标签函数中的参数识别 + assertReference(""" + + + hod="post" xpl:lib="/test/reference/a.xlib"/> + + + """, // + "/test/reference/a.xlib?method" // + ); + assertReference(""" + + + + hod="post"/> + + + """, // + "/test/reference/a.xlib?method" // + ); + assertReference(""" + hod="post"/> """, // - "/test/reference/a.xlib#DoFindByMdxQuery" // + "/test/reference/a.xlib?method" // ); } @@ -946,18 +977,6 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { """, // null // ); - -// // TODO 对 xpl 属性的文件引用 -// assertReference(""" -// -// """, // -// "/test/reference/a.xlib" // -// ); -// assertReference(""" -// -// """, // -// "/test/reference/a.xlib" // -// ); } public void testAttributeValueDefTypeReferences() { diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index ed8cfd3ab..f316bbbeb 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -11,6 +11,12 @@ + + + + + + -- Gitee From 0960e14aca60136f20f925fe9fc2c5a2b94dd6f6 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Tue, 22 Jul 2025 23:21:37 +0800 Subject: [PATCH 74/82] =?UTF-8?q?nop-idea-pugin:=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/nop/idea/plugin/services/NopAppListener.java | 2 +- nop-idea-plugin/src/main/resources/META-INF/plugin.xml | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java index d429488eb..4805f7f49 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java @@ -29,7 +29,7 @@ public class NopAppListener implements AppLifecycleListener { public void appFrameCreated(@NotNull List commandLineArgs) { // Note: 采用默认的加载器(ClassHelper#getDefaultClassLoader),在项目中会无法加载 IXplTagLib 的实现,原因未知 // TODO 在加载 DSL 时,是否会因为执行 x:gen-extends 等脚本而产生安全风险? - ClassHelper.registerSafeClassLoader((name) -> getClass().getClassLoader().loadClass(name)); + ClassHelper.registerSafeClassLoader((name) -> ClassHelper.forName(name, getClass().getClassLoader())); AppConfig.getConfigProvider().updateConfigValue(ApiConfigs.CFG_DEBUG, false); diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index e3485d3b7..ba46bc7d3 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -122,10 +122,5 @@ - - -- Gitee From c2881f28bc77c3ad335b2f9d84a52e1d86ab79c3 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 23 Jul 2025 17:39:04 +0800 Subject: [PATCH 75/82] =?UTF-8?q?nop-idea-pugin:=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=20XLang=20Xlib=20=E6=A0=87=E7=AD=BE=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E5=8F=82=E6=95=B0=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E3=80=81=E4=BB=A3=E7=A0=81=E8=A1=A5=E5=85=A8=E5=92=8C=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../idea/plugin/lang/psi/XLangAttribute.java | 11 +- .../io/nop/idea/plugin/lang/psi/XLangTag.java | 103 +++------- .../reference/XLangAttributeReference.java | 25 ++- .../lang/reference/XLangReferenceHelper.java | 8 +- .../reference/XLangXlibTagAttrReference.java | 57 ++++++ .../lang/reference/XLangXlibTagReference.java | 45 ++--- .../idea/plugin/lang/xlib/XlibTagMeta.java | 176 ++++++++++++++++++ .../plugin/lang/xlib/XlibXDefAttribute.java | 53 ++++++ .../nop/idea/plugin/utils/XmlPsiHelper.java | 6 +- .../doc/TestXLangDocumentationProvider.java | 32 ++++ .../plugin/lang/TestXLangCompletions.java | 21 +++ .../idea/plugin/lang/TestXLangReferences.java | 8 +- .../test/resources/_vfs/test/reference/a.xlib | 11 +- .../_vfs/test/reference/user.view.xml | 2 +- 14 files changed, 436 insertions(+), 122 deletions(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagAttrReference.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/xlib/XlibTagMeta.java create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/xlib/XlibXDefAttribute.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java index cc0e09e1c..2c7e35444 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -14,6 +14,8 @@ import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.SchemaPrefixReference; import com.intellij.psi.impl.source.xml.XmlAttributeImpl; import io.nop.idea.plugin.lang.reference.XLangAttributeReference; +import io.nop.idea.plugin.lang.reference.XLangXlibTagAttrReference; +import io.nop.idea.plugin.lang.xlib.XlibXDefAttribute; import io.nop.xlang.xdef.IXDefAttribute; import org.jetbrains.annotations.NotNull; @@ -51,7 +53,14 @@ public class XLangAttribute extends XmlAttributeImpl { int nameOffset = (ns.isEmpty() ? -1 : ns.length()) + 1; TextRange nameTextRange = TextRange.allOf(name).shiftRight(nameOffset); - XLangAttributeReference ref1 = new XLangAttributeReference(this, nameTextRange); + PsiReference ref1; + + IXDefAttribute defAttr = getDefAttr(); + if (defAttr instanceof XlibXDefAttribute xlibDefAttr) { + ref1 = new XLangXlibTagAttrReference(this, nameTextRange, xlibDefAttr); + } else { + ref1 = new XLangAttributeReference(this, nameTextRange, defAttr); + } return ref0 != null ? new PsiReference[] { ref0, ref1 } : new PsiReference[] { ref1 }; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index 4380f8204..f3237609e 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -16,7 +16,6 @@ import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.TagNameReference; @@ -32,6 +31,7 @@ import io.nop.idea.plugin.lang.XLangDocumentation; import io.nop.idea.plugin.lang.reference.XLangTagReference; import io.nop.idea.plugin.lang.reference.XLangXlibTagNsReference; import io.nop.idea.plugin.lang.reference.XLangXlibTagReference; +import io.nop.idea.plugin.lang.xlib.XlibTagMeta; import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.XDefPsiHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; @@ -50,7 +50,6 @@ import io.nop.xlang.xdsl.XDslConstants; import io.nop.xlang.xdsl.XDslKeys; import io.nop.xlang.xpl.XplConstants; import io.nop.xlang.xpl.xlib.XlibConstants; -import io.nop.xlang.xpl.xlib.XplLibHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -195,9 +194,16 @@ public class XLangTag extends XmlTagImpl { attrName = changeNamespace(attrName, getXDefKeys().NS, XDefKeys.DEFAULT.NS); } - // TODO 为 xlib 标签函数的参数构造定义 + IXDefAttribute attr = getXDefNodeAttr(getSchemaDefNode(), attrName); - return getXDefNodeAttr(getSchemaDefNode(), attrName); + if (attr == null || attr.isUnknownAttr()) { + XlibTagMeta xlibTag = getXlibTagMeta(); + + if (xlibTag != null) { + return xlibTag.getAttribute(attrName); + } + } + return attr; } /** @@ -293,7 +299,7 @@ public class XLangTag extends XmlTagImpl { } /** 若当前标签对应的是 xlib 的函数节点,则返回该函数节点信息 */ - protected XlibTagMeta getXlibTagMeta() { + public XlibTagMeta getXlibTagMeta() { String tagNs = getNamespacePrefix(); if (StringHelper.isEmpty(tagNs)) { return null; @@ -306,7 +312,6 @@ public class XLangTag extends XmlTagImpl { String lib; XmlElement ref = null; - boolean selfImported = false; if (XplConstants.XPL_THIS_LIB_NS.equals(tagNs)) { // Note: 单元测试内,可能得不到当前标签所在文件的 vfs 路径 lib = XmlPsiHelper.getNopVfsPath(this); @@ -329,9 +334,7 @@ public class XLangTag extends XmlTagImpl { XmlAttribute libAttr = getAttribute(XplConstants.ATTR_XPL_LIB); lib = libAttr != null ? libAttr.getValue() : null; - - selfImported = lib != null; - if (!selfImported) { + if (lib == null) { XLangTag importTag = XlibTagMeta.findXlibImportTag(this, tagNs); if (importTag != null) { @@ -349,7 +352,7 @@ public class XLangTag extends XmlTagImpl { String tagName = getLocalName(); - return new XlibTagMeta(ref, tagNs, tagName, lib, selfImported); + return new XlibTagMeta(ref, tagNs, tagName, lib); } /** 获取当前标签的说明文档 */ @@ -381,12 +384,15 @@ public class XLangTag extends XmlTagImpl { defNode = getSchemaDefNode(); } - // TODO 为 xlib 标签函数构造文档 - if (defNode == null) { return null; } + XlibTagMeta xlibTag = getXlibTagMeta(); + if (xlibTag != null) { + return xlibTag.getDocumentation(); + } + XLangDocumentation doc = new XLangDocumentation(defNode); doc.setMainTitle(tagName); @@ -430,7 +436,13 @@ public class XLangTag extends XmlTagImpl { return null; } - // TODO 为 xlib 标签函数的参数构造文档 + if (defAttr.isUnknownAttr()) { + XlibTagMeta xlibTag = getXlibTagMeta(); + + if (xlibTag != null) { + return xlibTag.getAttrDocumentation(attrName); + } + } XLangDocumentation doc = new XLangDocumentation(defAttr); doc.setMainTitle(mainTitle); @@ -589,71 +601,6 @@ public class XLangTag extends XmlTagImpl { return new RootTagSchemaMeta(project, schemaUrl, xdefKeys, xdslKeys, selfDef); } - protected static class XlibTagMeta { - private final XmlElement ref; - - /** xlib 标签函数的名字空间 */ - public final String tagNs; - /** xlib 标签函数的名字:不含名字空间 */ - public final String tagName; - - /** xlib 的 vfs 路径 */ - public final String xlibPath; - /** 是否在节点上通过 {@link XplConstants#ATTR_XPL_LIB} 属性导入 */ - private final boolean selfImported; - - XlibTagMeta(@NotNull XmlElement ref, String tagNs, String tagName, String xlibPath, boolean selfImported) { - this.ref = ref; - - this.tagNs = tagNs; - this.tagName = tagName; - - this.xlibPath = xlibPath; - this.selfImported = selfImported; - } - - private static String getXlibAlias(String path) { - try { - return XplLibHelper.getNamespaceFromLibPath(path); - } catch (Exception ignore) { - return null; - } - } - - public static XLangTag findXlibImportTag(XLangTag tag, String alias) { - XLangTag parentTag = tag != null ? tag.getParentTag() : null; - if (parentTag == null || !parentTag.isXplDefNode()) { - return null; - } - - for (PsiElement child : parentTag.getChildren()) { - if (!(child instanceof XLangTag childTag) // - || !XplConstants.TAG_C_IMPORT.equals(childTag.getName()) // - ) { - continue; - } - - XmlAttribute fromAttr = childTag.getAttribute(XplConstants.FROM_NAME); - String from = fromAttr != null ? fromAttr.getValue() : null; - if (from == null) { - continue; - } - - XmlAttribute asAttr = childTag.getAttribute(XplConstants.AS_NAME); - String as = asAttr != null ? asAttr.getValue() : null; - if (as == null) { - as = getXlibAlias(from); - } - - if (alias.equals(as)) { - return childTag; - } - } - - return findXlibImportTag(parentTag, alias); - } - } - private static class RootTagSchemaMeta extends SchemaMeta { protected final String schemaUrl; protected final XDefKeys xdefKeys; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java index 9844be3fa..67b1dba7f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java @@ -23,6 +23,8 @@ import com.intellij.util.PlatformIcons; import io.nop.api.core.util.SourceLocation; import io.nop.idea.plugin.lang.psi.XLangAttribute; import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.lang.xlib.XlibTagMeta; +import io.nop.idea.plugin.lang.xlib.XlibXDefAttribute; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xdef.IXDefAttribute; @@ -40,15 +42,15 @@ import org.jetbrains.annotations.Nullable; * @date 2025-07-10 */ public class XLangAttributeReference extends XLangReferenceBase { + private final IXDefAttribute defAttr; - public XLangAttributeReference(XLangAttribute myElement, TextRange myRangeInElement) { + public XLangAttributeReference(XLangAttribute myElement, TextRange myRangeInElement, IXDefAttribute defAttr) { super(myElement, myRangeInElement); + this.defAttr = defAttr; } @Override public @Nullable PsiElement resolveInner() { - XLangAttribute attr = (XLangAttribute) myElement; - IXDefAttribute defAttr = attr.getDefAttr(); if (defAttr == null) { return null; } @@ -113,6 +115,8 @@ public class XLangAttributeReference extends XLangReferenceBase { if (!usedXDslNs && xdslDefNode != null) { addDefAttr(result, xdslDefNode, attrNs, existAttrNames); } + // 引入可能的 xlib 标签函数的参数 + addDefAttr(result, tag.getXlibTagMeta(), existAttrNames); return result.stream() // .sorted((a, b) -> XLangReferenceHelper.XLANG_NAME_COMPARATOR.compare(a.name, b.name)) // @@ -152,6 +156,21 @@ public class XLangAttributeReference extends XLangReferenceBase { } } + private static void addDefAttr(List list, XlibTagMeta xlibTag, Set excludeNames) { + if (xlibTag == null) { + return; + } + + for (XlibXDefAttribute defAttr : xlibTag.getAttributes().values()) { + String attrName = defAttr.getName(); + if (excludeNames.contains(attrName)) { + continue; + } + + list.add(new DefAttrWithLabel(attrName, defAttr, defAttr.label)); + } + } + /** 注意,若当前属性已经包含完整的名字空间,则补全项必须移除其名字空间,否则,补全项的插入位置将会发生偏移 */ private static LookupElement lookupAttr(String attrName, String label, boolean trimNs) { if (trimNs) { diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java index b145dc899..ac3ae3079 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -59,11 +59,11 @@ import static io.nop.xlang.xdef.XDefConstants.XDEF_TYPE_PREFIX_OPTIONS; * @date 2025-07-12 */ public class XLangReferenceHelper { + /** 无命名空间的属性排在最前面,且 xdef 名字空间排在其他名字空间之前 */ public static final Comparator XLANG_NAME_COMPARATOR = (a, b) -> { int aNsIndex = a.indexOf(':'); int bNsIndex = b.indexOf(':'); - // 确保无命名空间的属性排在最前面,且 xdef 名字空间排在其他名字空间之前 if (aNsIndex <= 0 && bNsIndex <= 0) { return a.compareTo(b); } // @@ -221,7 +221,11 @@ public class XLangReferenceHelper { /** 对文本做默认的引用识别 */ public static PsiReference[] getReferencesFromText(XmlElement refElement, String refValue) { - if (!refValue.endsWith(".xdef")) { + if (!refValue.endsWith(".xdef") // + && !refValue.endsWith(".xpl") // + && !refValue.endsWith(".xgen") // + && !refValue.endsWith(".xrun") // + ) { return PsiReference.EMPTY_ARRAY; } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagAttrReference.java new file mode 100644 index 000000000..2f5826f6e --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagAttrReference.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang.reference; + +import java.util.function.Function; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import io.nop.api.core.util.SourceLocation; +import io.nop.idea.plugin.lang.psi.XLangAttribute; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.lang.xlib.XlibXDefAttribute; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import io.nop.xlang.xpl.xlib.XlibConstants; +import org.jetbrains.annotations.Nullable; + +/** + * 对 xlib 函数标签的参数的引用识别 + * + * @author flytreeleft + * @date 2025-07-23 + */ +public class XLangXlibTagAttrReference extends XLangReferenceBase { + private final XlibXDefAttribute defAttr; + + public XLangXlibTagAttrReference( + XLangAttribute myElement, TextRange myRangeInElement, XlibXDefAttribute defAttr + ) { + super(myElement, myRangeInElement); + this.defAttr = defAttr; + } + + @Override + public @Nullable PsiElement resolveInner() { + String path = XmlPsiHelper.getNopVfsPath(defAttr); + if (path == null) { + return null; + } + + SourceLocation loc = defAttr.getLocation(); + Function targetResolver = (file) -> { + XLangTag tag = XmlPsiHelper.getPsiElementAt(file, loc, XLangTag.class); + + return tag != null ? tag.getAttribute(XlibConstants.NAME_NAME) : null; + }; + + return new NopVirtualFile(myElement, path, targetResolver); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java index e2f489da0..21fc15cf3 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java @@ -11,19 +11,16 @@ package io.nop.idea.plugin.lang.reference; import java.util.function.Function; import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.lang.xlib.XlibTagMeta; import io.nop.idea.plugin.messages.NopPluginBundle; -import io.nop.idea.plugin.resource.ProjectEnv; import io.nop.idea.plugin.utils.LookupElementHelper; import io.nop.idea.plugin.utils.XmlPsiHelper; import io.nop.idea.plugin.vfs.NopVirtualFile; import io.nop.xlang.xpl.IXplTag; -import io.nop.xlang.xpl.IXplTagLib; -import io.nop.xlang.xpl.xlib.XplLibHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -59,14 +56,15 @@ public class XLangXlibTagReference extends XLangReferenceBase { return targetResolver.apply(myElement.getContainingFile()); } - // 得到标签函数实际定义所在的 xlib - IXplTagLib xlib = loadXlib(); - IXplTag xlibTag = xlib != null ? xlib.getTag(tagName) : null; - String targetXlibPath = XmlPsiHelper.getNopVfsPath(xlibTag); + NopVirtualFile target = XlibTagMeta.withLoadedXlib(myElement, xlibPath, (xlib) -> { + IXplTag xlibTag = xlib.getTag(tagName); + String targetXlibPath = XmlPsiHelper.getNopVfsPath(xlibTag); + + return targetXlibPath != null // + ? new NopVirtualFile(myElement, targetXlibPath, targetResolver) // + : null; + }, null); - NopVirtualFile target = targetXlibPath != null - ? new NopVirtualFile(myElement, targetXlibPath, targetResolver) - : null; if (target == null || target.hasEmptyChildren()) { String msg = NopPluginBundle.message("xlang.annotation.reference.xlib-tag-not-found", tagName, xlibPath); setUnresolvedMessage(msg); @@ -78,23 +76,12 @@ public class XLangXlibTagReference extends XLangReferenceBase { @Override public Object @NotNull [] getVariants() { - IXplTagLib xlib = loadXlib(); - if (xlib == null) { - return LookupElement.EMPTY_ARRAY; - } - - // Note: 标签函数允许递归引用,不需要排除当前标签 - return xlib.getTags().keySet().stream() // - .sorted() // - .map((name) -> LookupElementHelper.lookupXmlTag(name, null)) // - .toArray(); - } - - private IXplTagLib loadXlib() { - // xlib 是可扩展的,因此,需要直接加载 xlib 模型以获取准确的标签函数名 - // TODO 在插件内加载 DSL,是否会因为执行 x:gen-extends 等脚本而产生安全风险? - Project project = myElement.getProject(); - - return ProjectEnv.withProject(project, () -> XplLibHelper.loadLib(xlibPath)); + return XlibTagMeta.withLoadedXlib(myElement, xlibPath, (xlib) -> { + // Note: 标签函数允许递归引用,不需要排除当前标签 + return xlib.getTags().keySet().stream() // + .sorted() // + .map((name) -> LookupElementHelper.lookupXmlTag(name, null)) // + .toArray(); + }, LookupElement.EMPTY_ARRAY); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/xlib/XlibTagMeta.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/xlib/XlibTagMeta.java new file mode 100644 index 000000000..952bc0b98 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/xlib/XlibTagMeta.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang.xlib; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlElement; +import io.nop.idea.plugin.lang.XLangDocumentation; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.xlang.xpl.IXplTag; +import io.nop.xlang.xpl.IXplTagAttribute; +import io.nop.xlang.xpl.IXplTagLib; +import io.nop.xlang.xpl.XplConstants; +import io.nop.xlang.xpl.xlib.XplLibHelper; +import io.nop.xlang.xpl.xlib.XplTagAttribute; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author flytreeleft + * @date 2025-07-23 + */ +public class XlibTagMeta { + static final Logger LOG = LoggerFactory.getLogger(XlibTagMeta.class); + + public final XmlElement ref; + + /** xlib 标签函数的名字空间 */ + public final String tagNs; + /** xlib 标签函数的名字:不含名字空间 */ + public final String tagName; + + /** xlib 的 vfs 路径 */ + public final String xlibPath; + + public XlibTagMeta(@NotNull XmlElement ref, String tagNs, String tagName, String xlibPath) { + this.ref = ref; + + this.tagNs = tagNs; + this.tagName = tagName; + + this.xlibPath = xlibPath; + } + + public XlibXDefAttribute getAttribute(String attrName) { + return withLoadedXlib(ref, xlibPath, (xlib) -> { + IXplTag tag = xlib.getTag(tagName); + XplTagAttribute attr = tag != null ? (XplTagAttribute) tag.getAttr(attrName) : null; + if (attr == null) { + return null; + } + + return new XlibXDefAttribute(attr); + }, null); + } + + public Map getAttributes() { + return withLoadedXlib(ref, xlibPath, (xlib) -> { + IXplTag tag = xlib.getTag(tagName); + if (tag == null) { + return Map.of(); + } + + Map attrs = new HashMap<>(); + for (IXplTagAttribute attr : tag.getAttrs()) { + XlibXDefAttribute at = new XlibXDefAttribute((XplTagAttribute) attr); + + attrs.put(at.getName(), at); + } + + return attrs; + }, Map.of()); + } + + public XLangDocumentation getDocumentation() { + return withLoadedXlib(ref, xlibPath, (xlib) -> { + IXplTag tag = xlib.getTag(tagName); + if (tag == null) { + return null; + } + + XLangDocumentation doc = new XLangDocumentation(tag); + doc.setMainTitle(tagName); + doc.setSubTitle(tag.getDisplayName()); + doc.setDesc(tag.getDescription()); + + return doc; + }, null); + } + + public XLangDocumentation getAttrDocumentation(String attrName) { + XlibXDefAttribute attr = getAttribute(attrName); + + XLangDocumentation doc = new XLangDocumentation(attr); + doc.setMainTitle(attrName); + doc.setSubTitle(attr.label); + doc.setDesc(attr.desc); + + return doc; + } + + public static T withLoadedXlib( + PsiElement refElement, String xlibPath, // + Function consumer, T defaultValue + ) { + if (XmlPsiHelper.findPsiFilesByNopVfsPath(refElement, xlibPath).isEmpty()) { + return defaultValue; + } + + // xlib 是可扩展的,因此,需要直接加载 xlib 模型以获取准确的标签函数名 + // TODO 在插件内加载 DSL,是否会因为执行 x:gen-extends 等脚本而产生安全风险? + try { + IXplTagLib xlib = ProjectEnv.withProject(refElement.getProject(), () -> XplLibHelper.loadLib(xlibPath)); + + return consumer.apply(xlib); + } catch (Exception e) { + LOG.debug("nop.load-xlib-fail", e); + + return defaultValue; + } + } + + private static String getXlibAlias(String path) { + try { + return XplLibHelper.getNamespaceFromLibPath(path); + } catch (Exception ignore) { + return null; + } + } + + public static XLangTag findXlibImportTag(XLangTag tag, String alias) { + XLangTag parentTag = tag != null ? tag.getParentTag() : null; + if (parentTag == null || !parentTag.isXplDefNode()) { + return null; + } + + for (PsiElement child : parentTag.getChildren()) { + if (!(child instanceof XLangTag childTag) // + || !XplConstants.TAG_C_IMPORT.equals(childTag.getName()) // + ) { + continue; + } + + XmlAttribute fromAttr = childTag.getAttribute(XplConstants.FROM_NAME); + String from = fromAttr != null ? fromAttr.getValue() : null; + if (from == null) { + continue; + } + + XmlAttribute asAttr = childTag.getAttribute(XplConstants.AS_NAME); + String as = asAttr != null ? asAttr.getValue() : null; + if (as == null) { + as = getXlibAlias(from); + } + + if (alias.equals(as)) { + return childTag; + } + } + + return findXlibImportTag(parentTag, alias); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/xlib/XlibXDefAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/xlib/XlibXDefAttribute.java new file mode 100644 index 000000000..c340da2e4 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/xlib/XlibXDefAttribute.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang.xlib; + +import io.nop.commons.type.StdDataType; +import io.nop.xlang.xdef.XDefConstants; +import io.nop.xlang.xdef.XDefTypeDecl; +import io.nop.xlang.xdef.impl.XDefAttribute; +import io.nop.xlang.xdef.parse.XDefTypeDeclParser; +import io.nop.xlang.xpl.xlib.XplTagAttribute; + +/** + * @author flytreeleft + * @date 2025-07-23 + */ +public class XlibXDefAttribute extends XDefAttribute { + public final String label; + public final String desc; + + public XlibXDefAttribute(XplTagAttribute attr) { + this.label = attr.getDisplayName(); + this.desc = attr.getDescription(); + + setName(attr.getName()); + setPropName(attr.getVarName()); + setLocation(attr.getLocation()); + + StringBuilder sb = new StringBuilder(); + if (attr.isMandatory()) { + sb.append(XDefConstants.XDEF_TYPE_PREFIX_MANDATORY); + } + if (attr.isInternal() || attr.isDeprecated()) { + sb.append(XDefConstants.XDEF_TYPE_PREFIX_DEPRECATED); + } + if (attr.getStdDomain() != null) { + sb.append(attr.getStdDomain()); + } else { + sb.append(StdDataType.ANY.getName()); + } + if (attr.getDefaultValue() != null) { + sb.append('=').append(attr.getDefaultValue()); + } + + XDefTypeDecl type = new XDefTypeDeclParser().parseFromText(attr.getLocation(), sb.toString()); + setType(type); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 6af141f5b..1247ff901 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -96,10 +96,14 @@ public class XmlPsiHelper { } public static List findPsiFilesByNopVfsPath(PsiElement element, String path) { + if (element == null || path == null) { + return List.of(); + } + Project project = element.getProject(); String absPath = getNopVfsAbsolutePath(path, element); - return XmlPsiHelper.findPsiFileList(project, absPath); + return findPsiFileList(project, absPath); } /** 获取指定行列的 {@link PsiElement 元素} */ diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java index c419b387e..62047e4bb 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -156,6 +156,21 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { assertTrue(doc.contains("/test/doc/example.xdef")); } // ); + + // xlib 标签函数的文档 + assertDoc(""" + + + dByMdxQuery xpl:lib="/test/reference/a.xlib"/> + + + """, // + (doc) -> { + assertTrue(doc.contains("Find by MDX Query")); + assertTrue(doc.contains("for DoFindByMdxQuery")); + assertTrue(doc.contains("/test/reference/a.xlib")); + } // + ); } public void testGenerateDocForAttribute() { @@ -415,6 +430,23 @@ public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { assertTrue(doc.contains("/nop/schema/xpl.xdef")); } // ); + + // xlib 标签函数的参数文档 + assertDoc(""" + + + uilder="" + /> + + + """, // + (doc) -> { + assertTrue(doc.contains("Query Builder")); + assertTrue(doc.contains("for queryBuilder")); + assertTrue(doc.contains("/test/reference/a.xlib")); + } // + ); } public void testGenerateDocForXmlAttributeValue() { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java index ab40c42fa..46a7f8a71 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java @@ -786,6 +786,27 @@ public class TestXLangCompletions extends BaseXLangPluginTestCase { """); + + // 对标签函数参数的补全 + assertCompletion("queryBuilder", // + """ + + + + /> + + + """, // + """ + + + + + + """); } /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */ diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 16a1da7cf..54941d04c 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -322,7 +322,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { "thisLib:_DoSomething" // ); - // - TODO 标签函数中的参数识别 + // - 标签函数中的参数识别 assertReference(""" @@ -330,7 +330,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { """, // - "/test/reference/a.xlib?method" // + "/test/reference/a.xlib?attr#name=method" // ); assertReference(""" @@ -340,7 +340,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { """, // - "/test/reference/a.xlib?method" // + "/test/reference/a.xlib?attr#name=method" // ); assertReference(""" @@ -350,7 +350,7 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { """, // - "/test/reference/a.xlib?method" // + "/test/reference/a.xlib?attr#name=method" // ); } diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib index f316bbbeb..8b2ad0a8b 100644 --- a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -13,15 +13,20 @@ - + - + + - + + This is description for queryBuilder + + This is description for DoFindByMdxQuery + - + -- Gitee From a3e746b8eb693bf98d8aa188018ccb16a7e08caf Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 23 Jul 2025 20:26:47 +0800 Subject: [PATCH 76/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7=E5=80=BC=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/lang/psi/XLangAttributeValue.java | 6 +++-- .../XLangStdDomainGenericTypeReference.java | 6 ++++- .../idea/plugin/lang/TestXLangReferences.java | 22 +++++++++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index 8a5e126cd..af8e26370 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -21,6 +21,7 @@ import io.nop.idea.plugin.lang.reference.XLangXdefNameReference; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.XDefKeys; import io.nop.xlang.xdsl.XDslKeys; +import io.nop.xlang.xpl.utils.XplParseHelper; import org.jetbrains.annotations.NotNull; /** @@ -51,7 +52,7 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { @Override public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { String attrValue = getValue(); - if (StringHelper.isEmpty(attrValue)) { + if (StringHelper.isEmpty(attrValue) || XplParseHelper.hasExpr(attrValue)) { return PsiReference.EMPTY_ARRAY; } @@ -63,7 +64,8 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { IXDefAttribute defAttr = attr.getDefAttr(); // 对于未定义属性,不做引用识别 if (defAttr == null) { - return PsiReference.EMPTY_ARRAY; + //return PsiReference.EMPTY_ARRAY; + return XLangReferenceHelper.getReferencesFromText(this, attrValue); } // 根据属性名,从属性值中查找引用 diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.java index 45139f36a..a32722939 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.java @@ -11,6 +11,7 @@ package io.nop.idea.plugin.lang.reference; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import io.nop.core.type.IGenericType; +import io.nop.core.type.impl.PredefinedPrimitiveType; import io.nop.core.type.parse.GenericTypeParser; import io.nop.idea.plugin.utils.PsiClassHelper; import org.jetbrains.annotations.NotNull; @@ -42,7 +43,10 @@ public class XLangStdDomainGenericTypeReference extends XLangReferenceBase { return null; } - return PsiClassHelper.findClass(myElement, type.getClassName()); + String className = type instanceof PredefinedPrimitiveType p + ? p.getStdDataType().getJavaClass().getName() + : type.getClassName(); + return PsiClassHelper.findClass(myElement, className); } @Override diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 54941d04c..012120af2 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -969,17 +969,31 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { """, // null // ); - // - 属性未定义,引用无法识别 +// // - 属性未定义,引用无法识别:TODO 后续完成对 c:if 等内置函数的识别后,取消对普通 vfs 的文本识别 +// assertReference(""" +//

+// +// +// """, // +// null // +// ); + + // 含 ${} 表达式的值,将被忽略 assertReference(""" -
- - + + + """, // null // ); } public void testAttributeValueDefTypeReferences() { + assertReference(insertCaretIntoVfs("/test/reference/a.xlib", // + "lean\""), // + "java.lang.Boolean" // + ); assertReference(insertCaretIntoVfs("/nop/schema/xdef.xdef", // "xdef:default-override=\"enum:io.nop.xlang.xdef.XDefOverride\"", // "xdef:default-override=\"enum:io.nop.xlang.xdef.XDefOverride\""), // -- Gitee From 4d9249d958e2e6171427fcb916329df766c1ce81 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Wed, 23 Jul 2025 20:34:18 +0800 Subject: [PATCH 77/82] =?UTF-8?q?nop-idea-pugin:=20=E4=B8=8D=E5=AF=B9?= =?UTF-8?q?=E5=90=AB=20${xx}=20=E8=A1=A8=E8=BE=BE=E5=BC=8F=E7=9A=84?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=80=BC=E5=81=9A=E6=9C=89=E6=95=88=E6=80=A7?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/nop/idea/plugin/annotator/XLangAnnotator.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java index 2f17db54f..bc86d2d0d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java @@ -39,6 +39,7 @@ import io.nop.xlang.xdef.IStdDomainHandler; import io.nop.xlang.xdef.IXDefAttribute; import io.nop.xlang.xdef.XDefTypeDecl; import io.nop.xlang.xdef.domain.StdDomainRegistry; +import io.nop.xlang.xpl.utils.XplParseHelper; import org.jetbrains.annotations.NotNull; public class XLangAnnotator implements Annotator { @@ -247,6 +248,9 @@ public class XLangAnnotator implements Annotator { } propValue = StringHelper.unescapeXml(propValue); + if (XplParseHelper.hasExpr(propValue)) { + return; + } try { domainHandler.validate(loc, propName, propValue, IValidationErrorCollector.THROW_ERROR); -- Gitee From 8dcf8dfc07cfc9c018a1e1ea4d98a9f9c44e0f72 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 24 Jul 2025 17:19:15 +0800 Subject: [PATCH 78/82] =?UTF-8?q?nop-idea-pugin:=20=E6=B7=BB=E5=8A=A0=20XL?= =?UTF-8?q?ang=20=E9=87=8D=E5=91=BD=E5=90=8D=E4=BB=A3=E7=A0=81=EF=BC=88?= =?UTF-8?q?=E6=9C=AA=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lang/XLangElementRenameProcessor.java | 73 +++++++++ .../io/nop/idea/plugin/lang/psi/XLangTag.java | 21 +++ .../lang/reference/XLangXlibTagReference.java | 9 + .../nop/idea/plugin/vfs/NopVirtualFile.java | 28 +++- .../src/main/resources/META-INF/plugin.xml | 1 + .../nop/idea/plugin/lang/TestXLangRename.java | 154 ++++++++++++++++++ 6 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangElementRenameProcessor.java create mode 100644 nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangRename.java diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangElementRenameProcessor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangElementRenameProcessor.java new file mode 100644 index 000000000..5ad000d1d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangElementRenameProcessor.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang; + +import java.util.Map; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.search.SearchScope; +import com.intellij.refactoring.rename.RenamePsiElementProcessor; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import org.jetbrains.annotations.NotNull; + +/** + * XLang 元素重命名的处理器 + *

+ * 默认的重命名处理器 {@link RenamePsiElementProcessor#DEFAULT} + * 会调用元素的 {@link PsiNamedElement#setName(String)} + * 方法完成对重命名节点本身的修改,但不会查找并修改关联方,需要在 + * {@link #prepareRenaming} 中完成对关联方的收集 + * + * @author flytreeleft + * @date 2025-07-23 + */ +public class XLangElementRenameProcessor extends RenamePsiElementProcessor { + + @Override + public boolean canProcessElement(@NotNull PsiElement element) { + return element instanceof NopVirtualFile; + } + + @Override + public void prepareRenaming( + @NotNull PsiElement element, @NotNull String newName, @NotNull Map allRenames, + @NotNull SearchScope scope + ) { + if (element instanceof NopVirtualFile vfs) { + vfs.prepareRenaming(newName, allRenames); + return; + } + +// XLangDocument root = PsiTreeUtil.getParentOfType(element, XLangDocument.class); +// if (root == null) { +// return; +// } +// +// PsiElement target = element; +// PsiElementVisitor visitor = new PsiElementVisitor() { +// @Override +// public void visitElement(@NotNull PsiElement element) { +// for (PsiReference ref : element.getReferences()) { +// if (ref instanceof XLangXlibTagReference || ref instanceof XLangTagReference) { +// PsiElement resolved = ref.resolve(); +// +// if (resolved == target) { +// allRenames.put(ref.getElement(), newName); +// } +// } +// } +// +// element.acceptChildren(this); +// } +// }; +// +// root.acceptChildren(visitor); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java index f3237609e..c82227ab3 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -16,6 +16,7 @@ import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceService; import com.intellij.psi.impl.source.xml.TagNameReference; @@ -23,6 +24,7 @@ import com.intellij.psi.impl.source.xml.XmlTagImpl; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlToken; +import com.intellij.util.IncorrectOperationException; import com.intellij.xml.util.XmlTagUtil; import io.nop.api.core.util.SourceLocation; import io.nop.commons.util.StringHelper; @@ -113,6 +115,25 @@ public class XLangTag extends XmlTagImpl { return text != null ? text.getTextChars() : ""; } + @Override + public PsiElement setName(@NotNull String name) throws IncorrectOperationException { + String tagName = getName(); + if (getXDefKeys().UNKNOWN_TAG.equals(tagName)) { + return this; + } + + String newName = name; + // 保留名字空间 + if (name.indexOf(':') <= 0) { + String ns = getNamespacePrefix(); + + if (!ns.isEmpty()) { + newName = ns + ':' + newName; + } + } + return super.setName(newName); + } + @Override public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { List refs = new ArrayList<>(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java index 21fc15cf3..57ef83882 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java @@ -14,6 +14,7 @@ import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.util.IncorrectOperationException; import io.nop.idea.plugin.lang.psi.XLangTag; import io.nop.idea.plugin.lang.xlib.XlibTagMeta; import io.nop.idea.plugin.messages.NopPluginBundle; @@ -84,4 +85,12 @@ public class XLangXlibTagReference extends XLangReferenceBase { .toArray(); }, LookupElement.EMPTY_ARRAY); } + + /** 对关联的源元素进行更名处理 */ + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + XLangTag element = (XLangTag) myElement; + + return element.setName(newElementName); + } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java index 05bbb6e72..12b8cafab 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java @@ -1,6 +1,7 @@ package io.nop.idea.plugin.vfs; import java.util.Arrays; +import java.util.Map; import java.util.Objects; import java.util.function.Function; import javax.swing.*; @@ -136,10 +137,35 @@ public class NopVirtualFile extends PsiElementBase implements PsiNamedElement { return getPath(); } + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @Override public PsiElement setName(@NotNull String name) throws IncorrectOperationException { - return null; + // 对当前元素本身不做更名 + return this; + } + + @Override + public boolean isWritable() { + // 允许将该元素视为可更名元素 + return true; + } + + /** + * 将关联的源元素和目标元素加入到待更名队列 + *

+ * 重命名处理器将会为待更名元素查找相关的 + * {@link com.intellij.psi.PsiReference PsiReference},从而对各个关联元素也做相应的更名处理 + */ + public void prepareRenaming(@NotNull String newName, @NotNull Map allRenames) { + allRenames.put(srcElement, newName); + + for (PsiElement child : getChildren()) { + if (!(child instanceof PsiFile)) { + allRenames.put(child, newName); + } + } } + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @Override public @NotNull Language getLanguage() { diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index ba46bc7d3..60c792532 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -95,6 +95,7 @@ + diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangRename.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangRename.java new file mode 100644 index 000000000..92ce26989 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangRename.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2017-2024 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ +package io.nop.idea.plugin.lang; + +import io.nop.idea.plugin.BaseXLangPluginTestCase; + +/** + * @author flytreeleft + * @date 2025-07-23 + */ +public class TestXLangRename extends BaseXLangPluginTestCase { + + public void testRenameXdefName() { + } + + public void testRenameTag() { + } + + public void testRenameXlibTag() { + // 从定义侧更名 + assertRename("NewCall", // + """ + + + ll> + + + + + + + + """, // + """ + + + + + + + + + + + """ // + ); + + // 从调用侧更名 + assertRename("NewCall", // + """ + + + + + + ll/> + + + + + """, // + """ + + + + + + + + + + + """ // + ); + } + + public void testRenameXlibAttr() { + // 从定义侧更名 + assertRename("newArg", // + """ + + + + + + + + + + + + + """, // + """ + + + + + + + + + + + + + """ // + ); + + // 从调用侧更名 + assertRename("newArg", // + """ + + + + + + + + g1="abc"/> + + + + + """, // + """ + + + + + + + + + + + + + """ // + ); + } + + protected void assertRename(String newName, String text, String expectedText) { + configureByXLangText(text); + myFixture.renameElementAtCaret(newName); + + myFixture.checkResult(expectedText); + } +} -- Gitee From de2bf00d08be16659f819d3bd839f762c7d3c55c Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 24 Jul 2025 17:39:38 +0800 Subject: [PATCH 79/82] =?UTF-8?q?nop-idea-pugin:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AF=B9=20XLang=20=E5=B1=9E=E6=80=A7=E5=80=BC=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=20vfs=20=E8=B7=AF=E5=BE=84=E6=96=87=E6=9C=AC=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/lang/psi/XLangAttributeValue.java | 3 ++- .../lang/reference/XLangReferenceHelper.java | 1 + .../idea/plugin/lang/TestXLangReferences.java | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java index af8e26370..99b4ba655 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -63,8 +63,9 @@ public class XLangAttributeValue extends XmlAttributeValueImpl { IXDefAttribute defAttr = attr.getDefAttr(); // 对于未定义属性,不做引用识别 - if (defAttr == null) { + if (defAttr == null || (defAttr.isUnknownAttr() && defAttr.getType().getStdDomain().equals("any"))) { //return PsiReference.EMPTY_ARRAY; + // Note: 临时支持对 xpl 内置函数的 vfs 引用识别 return XLangReferenceHelper.getReferencesFromText(this, attrValue); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java index ac3ae3079..6e125a39b 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -225,6 +225,7 @@ public class XLangReferenceHelper { && !refValue.endsWith(".xpl") // && !refValue.endsWith(".xgen") // && !refValue.endsWith(".xrun") // + && !refValue.endsWith(".xlib") // ) { return PsiReference.EMPTY_ARRAY; } diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java index 012120af2..1d09a58a2 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -986,6 +986,26 @@ public class TestXLangReferences extends BaseXLangPluginTestCase { """, // null // ); + + // 对 xpl 内置函数标签的属性值的识别 + assertReference(""" + + + + + + """, // + "/test/reference/a.xlib" // + ); + assertReference(""" + + + + + + """, // + "/test/reference/a.xlib" // + ); } public void testAttributeValueDefTypeReferences() { -- Gitee From 74c8afe0651fb667bc386cd7f72b7f19a6e46ceb Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 24 Jul 2025 20:20:48 +0800 Subject: [PATCH 80/82] nop-idea-pugin: update README.md --- nop-idea-plugin/README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nop-idea-plugin/README.md b/nop-idea-plugin/README.md index a28fea251..f9d42bbb1 100644 --- a/nop-idea-plugin/README.md +++ b/nop-idea-plugin/README.md @@ -1,5 +1,20 @@ -# 调试 +# Nop IDEA Plugin -* 执行gradle runIde指令调试 +## 构建 -* 执行 gradle buildPlugin指令打包到build/distributions目录下 \ No newline at end of file +在当前项目的根目录下执行 `gradle buildPlugin` 或 `bash ./gradlew buildPlugin` +指令,最终的构建产物将被打包到 `build/distributions` 目录下。 + +## 调试 + +将本项目导入到 IntelliJ IDEA 中,点开 `Gradle` 工具窗口。 +依次展开 `Tasks -> intellij`,再选中 `runIde`。 +接着,点击右键并选择 `Debug 'nop-idea-plugin [run...'`, +其将启动一个新的 IntelliJ IDEA 实例,并在其中自动安装/更新该插件。 + +> 新 IDEA 实例的版本与当前插件在 `build.gradle.kts` 中所配置的 `intellij` 的版本一致, +> 首次启动,将会自动下载并安装该版本实例(安装位置如 +> `$HOME/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC`)。 + +后续,在项目代码所在 IDEA 中添加断点,并在新 IDEA 实例中操作, +便可以像调试普通代码一样对当前插件进行调试。 -- Gitee From e77f1f8baedf239b3c2e68720571ae4afa2a7428 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 24 Jul 2025 22:45:59 +0800 Subject: [PATCH 81/82] =?UTF-8?q?nop-idea-pugin:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=E7=B1=BB=E5=8A=A0=E8=BD=BD=E5=99=A8=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E4=BA=A7=E7=94=9F=E4=B8=8D=E5=90=8C=E7=9A=84=20PsiCla?= =?UTF-8?q?ss=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nop/idea/plugin/services/NopAppListener.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java index 4805f7f49..ee90c8243 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java @@ -7,6 +7,8 @@ */ package io.nop.idea.plugin.services; +import java.util.List; + import com.intellij.ide.AppLifecycleListener; import io.nop.api.core.ApiConfigs; import io.nop.api.core.config.AppConfig; @@ -21,15 +23,23 @@ import io.nop.idea.plugin.resource.ProjectResourceComponentManager; import io.nop.idea.plugin.resource.ProjectVirtualFileSystem; import org.jetbrains.annotations.NotNull; -import java.util.List; - public class NopAppListener implements AppLifecycleListener { @Override public void appFrameCreated(@NotNull List commandLineArgs) { // Note: 采用默认的加载器(ClassHelper#getDefaultClassLoader),在项目中会无法加载 IXplTagLib 的实现,原因未知 // TODO 在加载 DSL 时,是否会因为执行 x:gen-extends 等脚本而产生安全风险? - ClassHelper.registerSafeClassLoader((name) -> ClassHelper.forName(name, getClass().getClassLoader())); + ClassHelper.registerSafeClassLoader((name) -> { + try { + return ClassHelper.forName(name); + } catch (ClassNotFoundException e) { + try { + return Class.forName(name); + } catch (ClassNotFoundException ignore) { + } + throw e; + } + }); AppConfig.getConfigProvider().updateConfigValue(ApiConfigs.CFG_DEBUG, false); -- Gitee From 70bca906170c56c0f754546d2ba5e03c31c856e3 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Thu, 24 Jul 2025 22:47:06 +0800 Subject: [PATCH 82/82] docs: update idea-plugin.md --- docs/user-guide/idea/attr-doc.png | Bin 24253 -> 32963 bytes docs/user-guide/idea/idea-check-1.jpg | Bin 0 -> 8898 bytes docs/user-guide/idea/idea-check.jpg | Bin 34833 -> 6759 bytes docs/user-guide/idea/idea-completion-1.jpg | Bin 0 -> 34286 bytes docs/user-guide/idea/idea-completion-2.jpg | Bin 0 -> 32560 bytes docs/user-guide/idea/idea-completion.jpg | Bin 85890 -> 24247 bytes docs/user-guide/idea/idea-link-1.png | Bin 0 -> 53192 bytes docs/user-guide/idea/idea-link.png | Bin 51655 -> 50945 bytes docs/user-guide/idea/idea-plugin.md | 74 +++++++++++++++------ docs/user-guide/idea/idea-quick-doc-1.jpg | Bin 0 -> 42240 bytes docs/user-guide/idea/idea-quick-doc.jpg | Bin 60121 -> 27689 bytes docs/user-guide/idea/idea-xscript-1.png | Bin 0 -> 52858 bytes docs/user-guide/idea/idea-xscript-2.png | Bin 0 -> 56076 bytes docs/user-guide/idea/idea-xscript-3.png | Bin 0 -> 62585 bytes docs/user-guide/idea/idea-xscript.png | Bin 0 -> 48505 bytes docs/user-guide/idea/link-ref.png | Bin 12645 -> 26473 bytes docs/user-guide/idea/node-doc.png | Bin 20572 -> 30299 bytes 17 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 docs/user-guide/idea/idea-check-1.jpg create mode 100644 docs/user-guide/idea/idea-completion-1.jpg create mode 100644 docs/user-guide/idea/idea-completion-2.jpg create mode 100644 docs/user-guide/idea/idea-link-1.png create mode 100644 docs/user-guide/idea/idea-quick-doc-1.jpg create mode 100644 docs/user-guide/idea/idea-xscript-1.png create mode 100644 docs/user-guide/idea/idea-xscript-2.png create mode 100644 docs/user-guide/idea/idea-xscript-3.png create mode 100644 docs/user-guide/idea/idea-xscript.png diff --git a/docs/user-guide/idea/attr-doc.png b/docs/user-guide/idea/attr-doc.png index 8a70910de642a9f8a2db734b8d1668c8684a0aeb..d4be818d94af006dabe13af8ad621bdfad87411e 100644 GIT binary patch literal 32963 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 literal 24253 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 diff --git a/docs/user-guide/idea/idea-check-1.jpg b/docs/user-guide/idea/idea-check-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7bad78b3fa08755c43018b05ba7caec4c161d5d8 GIT binary patch literal 8898 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 EANY=ri~s-t literal 34833 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 JBkr%c{{il@JRSf5 literal 0 HcmV?d00001 diff --git a/docs/user-guide/idea/idea-completion.jpg b/docs/user-guide/idea/idea-completion.jpg index dd0a6f03e7e9e7981ce84b6ccfb3480a2bdd4fe3..51119eb8a459fb363804bf6e2f49172da06ba7cb 100644 GIT binary patch literal 24247 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 a%=U}9jpr98Isc$^_n&AxU+w{Z8vPSP1j8Z# literal 85890 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®>aU0qsIu+TZg&Rg5YgHuj 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 UvG-@cAIo5BVtKjdlINfQ2ZljRm;e9( diff --git a/docs/user-guide/idea/idea-link-1.png b/docs/user-guide/idea/idea-link-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5c412f8e42d29f3bc8f84a8fdbd42ad46c9125b8 GIT binary patch literal 53192 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 QW(FYeboFyt=akR{0MF5>TmS$7 literal 0 HcmV?d00001 diff --git a/docs/user-guide/idea/idea-link.png b/docs/user-guide/idea/idea-link.png index d06e1a9534d475b1ca0ef1038ba9cf50256202c3..abcede568cc8d135e07192d9e281e0b9fbeab719 100644 GIT binary patch literal 50945 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 KSdEBL@c#h00&Xk- literal 51655 zcmd421yoyGyYF3ZDOTLc1`32ytT+@XRv@K#p~a=e-8~ykfffo4Qrv=TaZRz}?(R+q z?jbk8e)qf2x#xW2jC;TFjc<*?fP`eOx#pVdng8ea{AZAof(-6s%Ez~E-NKcVl~TEN z>n`lpt=nmj9$?-XS}n21{JQO+A|r9Dpoe-L^WvU~_*?N?w~E8CFAVNuUSrwHesH*T zikASf!;@t{o>g?MjsAbBF^DtlG(QdRp>ECu=Cc=4BFQSvx5_ z@|=;5cTIR+%78=~)cjSzoQM7cV&OdVgB(wx4|ZqB9%UplWLzq3)O}6i@&0F9qAD9A znmG@;VgmOc1R~EQ%=(__>Ivv;X9_2s1x`n&Fb!t7{peHu}T&6ukDl6O_&}(ek(%4Yds_xH&IMe>a}L&reQ+ z2}x|94}uvRlUC3^eH20b-^?$5oE28~l1aQX{4s+@2c5tn{EmSGGX>?x@5w=B;`W06 z?A$vpeNwNQmCXH9pjWdobr}nWe)>8^M%gbc@Bua+Y-2}d? zLDwyzodWP-8~yLWoFom~y^KgS3hZH{YIa!|I51a4L31iF8?NRBULc|r^+MyLgr7p) z+kR!?hIbOE_(iufq|b9fDfNMj^z;+&A8(NezzYd$atWP~%$koCfv8M^6JOJv*@ozl zAQ9*T-)!b@=8`#-OvA5OBvzBqM7XCFM|@VC!RxW|J?zKccdf$x>}) zicw4s$*`b#|j?nZU zFMm+JXxVT3l})A~DVd#&3$uS@Gt|83ds)aXLj^j#o1+UneA(krQ#(8OJElD0OaQrW z;Wy*nsAT?U3RG6l(29gF!BVNp!nM>OO>VQw-=sG&oBb$#z5GL`diCXDz1Pgg;OxhG zwQH@L_#Mj0R}WGC{p-0zCUZ&XwPGw*51wy+MXYyLT%Ng{2CT9T9ZEyTV7VDivVur0 zTbpMYB7wm>nn1x*TYzKAsy-tKHv;AC|==8OJOKH$?Y^L7aZZgHSwbIfz_JO z%IjIKhZ7H34f5q=_6U!087s*1XDB_-6f6763Lk{7Xs0u`n#`&~iDO(SZ5+&s3pMhf z5xUy7{9o5S$of=k&n6i?24lnyPP=+%TYNg!%s;H38GWcvB{pn5bW=tHHKTTUBS=fc znt#IcdEZQE)bI9h*E;a-%qF&D*JBoAqvmJ(3*C` zxu$t<%5?G_gmzbZULqSO{gj69Ac)UPW@B!fyJ18lV1&VX_r$%_n|G3elV2SVEBaHPiiIxjYdS`!itgjo5E0b zE3`($o}GuL=V;eNf6~v(BS!kvufZupzv75NMBG1%yx?~;5pU3`#y}l0Vn(VXgZ^w` zwNUD^@6kT7k_$+K{Rl`o%lLg$O2SUc9LWS;+#tr~HA0$>qLd*rN|$IlEr%f$xt>>t z))exa`t3TNZ!edRmmka~e)Em!Rw%x3=NXIe+t$J@4j72^s2h3Fy893ZCWINcJfu=} zDM~D@+ed@rY&VXmO7^i?;aGQN4V=7;0!zGth@ij22*|s`#O6Iu+vG&`jH+F%4ada#NRUckrBIzY&yrMfADpI?5yQgOr>f^8*x& zHWukBOZNM{?WyrI0dq&Ex8F-J;i^DCLV1u4gwtn2SGpM& zyl8%UHJ{f>)yRkJfG$tVXa@Cu#+en8-`&_I?!|C^M z!=N+|T6>rdqnAg+;<)z9aVvVNb{1v47P!1z-K6|)+jyLY92OW*G4+5HEam1ZA07o{)wr*dMP+K?xFd` zvX;SZ6h)TgM&quL%{+(;sA`F}Z6ObpAqRp* zRYmE^R`N-0N^M15+Pr#$?CK7s2(nr)Vn)oTCO>@PDSkd~$K4saRj$7atqg&|5#H;{88HXB)wJuwQeZQB6Tj6t31hKHwS&A0g@gRfuM;Nl!ywW zW@sCqA;=BHY^SgWxZ}&SIGok_CGdbqY(mlc5hK6kcj&DVZt&#{G zX+;85Gt-qlFJ8TbWTAu4Tz>Is=|`kTdZO8l!Px#PtW$T{9dR`Ek$g>co!OVJL*O=l zU$t(BW^&N*tBs-@WOP8&p^jw=tN;B-A**G4(8>cM>)9Re zB+huuszr%esj0Z2fwUB;1$$m+jm$N`p&a|K6BN6rQ)jR-f*`C^yWj5neBamUb$7jO zR&LKx07&|eOBWf3i-aePbQE#DPQjDsyJ?7!B&@+vv&6XiU~$#CM^-B09PA>b2_56g zrKX`Pz;f!qC-*!52C4-lmEXn_A;uDb z#zzRsQuU9@^sK`joee51h%IYeLctJ>t{UP)*QfrLKx z5v3Featq|0Nvsr8zTdl>utykfrTOATz0$Fv1f z#TiDi-x9Giw}$XFQl*5u1b!ch7UN`UcVHsT>_3UNrc#$9(G5+ zu7S5Zn*&LDCkH59k?o`PMWgK2g)$|o+!2f;O0n_S(Vj`$g|7-$E)*CUiOSAf5R%5k z0kwKjKhYWpYh}2wE{@b?=(o;1TaCGuLxn(9`qXzAg`=MqpaSdg?fpcK%6rJcp>EI@ zLG75%Mp|d$-c05x%x)g2dY0!2v}%+Uw-tu{4%X{rs7~`?;4a>Cid}8xqmxvHZ26bf zv4tg0HTS429)?db14537#x@aBXTrOR(%*Vwx(%4CKeY+#K8$mkxBZ~k zwgmt_5^7`$`$cR+SOJOm+rWp#28EhecV`_dI1bgNJ^C;KpaHQCxgQFTzi1Xx${hz7 zS|N$7WZzpU2MoMFGC`l`g2n%|#pz_o{Vt%L2ESw_1X)SC{;~0c()oF1VKRcVCqX!b z?cIEmO_k)jIYtbR{36~n@=;L>RXEk%=){^LwE!M^)0XmPJ}is;8M`@j98zZLRv!Jb z!|OEhWZQN7_slng*7>&HaC;L$YwgXQz(Mu+kRq`s1pNyb0X}A%QraG9qkAydDk;rr z7N{dlA0{!O)nR82Cjlg8wKx^Fx-3-*(o|~K*$T1(KssXtFW%%tsQ&q8l_q8?v1Tn9 z0nl~LaBJ|I7is}XFm-jK#>!Wpgt|ixVz=VoYBFBY^M;xQ82gB!vr;4^e zz3n!usnzMBOiN1*<bW|HPAm!nAUqk8mt6k!h;)y)d=?84Jrxl7S>X2>U714H7 z+Fz^M9*I0NdXX)Otwlvf`=Q+uny@m*pix|Y>^3NoJ#(IGc>LB0EIOQgJ&aTe*~r4U z8dgUn|K5uK0Eie8kIYRsNz0kVfpBx%K7Bc&wBZGJL}0EO=rQKfc65e}Zjt>tI%gwQ z|HvMM`G3?R;WEnz#4!1%wSal-N|>rnF&!TzB*^KFojiUVx{UKmJ9O66G5Y zK%h$Cz;G#bidwIsgjrrwB$*`b?v!Ks8&4a@MRmyoN!!SDVxA){E@2j#`-k(nTFy98 z&FDi0`iak1OLVEyv-X~k??Ub$d(Jy%+;?#%Nd+;-$CmSl@Ohn#R}qfc6Z(a|a!!Ei z?b$d3*(fWS?yOTYDY!I169{=&WK7Rt)GR0^?ho=$Lw*21u|x?&jd_N0Xf$$ro_Aoi zd@_er!04WwL{WlZQYH7N7s?a6-NYsT96sWrJ^fNvhcC$@nO&-hoGl{;EuX{MNd!2Y z7=dOjt~|{3{Io;KTpwS55&@|^)2tYp8;4TkK5fdnYoav}{W6@KfFPh16CW4?Rg(H_ z&k*vg+XyUwRl)CTBg%rE*4RCP@ei57Zr&D-R=Bg6k@r`r!j0{1qpM!T_7;g=mF7$O zfn^5&tWbL#KOp=t$2SVlV@~6?^I=;_b8NMaG}|~(C+Su4P<&H4PRfv2T;@y7Sip;y z0>KVa7KSe~)h1_F-(thUdsuK?LJxxd2Oh0LNu6-!FHV(l-Zue0XdNk4o6F#70HMLh z8X=xws~+q#9|dpqD9dmpVj}st9J!xd$Jhj(1Odm?3KNOW2Jg-*QhkFi@ilspVO`e$8#ASY3`V=tx=`KUww!7KBhmX^L4=r_TKY2^!J)1O$UlJUjgS zwm0J?x?E$Dd9{JWj!y?WxU2;Edu*mv%x~>8$udFUU}1UWW0>Obc>djUdHtj5r&)*YKbm6ZSQ!d!g zt@d^A3(Sh6N-uQk$?UtNvN<*XA5g~Lcx;?BDXHCGO6GHKAHhTuZOt2y z2v002=-EmEqmDEwwkZ#RAC42B30zF(QA_8N8$$OKsQcwJmTT}<>sT3L<;ou@G5dC@ zNUW#1^v>`3DehjbWq;d*J}B6vdRA=Dfm!#uMJj#z!(V-sl&s@Cj{-=d%sqxB(|oezp~HtZ4Kr%5Q=JbKeqM z8Pn3;@OyLGh?Uhs!#6zx66*);?n`42caIJ| zTn|2y4t`&(v`xm6Ch!gOtY66Rs^s)03o~BjIlLFx{96UAUzH8CnS@k`5hDwKWNw^q z9?6i|W_5n)`gS~O7=;8|s$gm2={0t@x4LfVyk1*vHxD)jYsmv6lY-V)2n8Nk&7fv| z$`QuzpZWGyjh6QoeLfPlyuvw1u72+#!jV`UblAHPMn!QmfeH3{4tK6;qG?k+4D!Xt zD03Tk487B2zXW(G@UPD(mc~QVwZAAHFP>-OSz?`vhkX7#LVAPRq&gV}WbdUeM~xV^ z=8~aK_S7yTAh)legCUUjml7s@e0dEzWij#Z3M!{AH(=hBl5+o>+Y%7uQ~%W#=6xwv zNXpPHNfrzoCcrdeaMQ@uKT7TYzx|5x^Z&pu=~MM@psgvJ%0P*7UD<8V;|?L3$jQ~W89*}uk(n^0m_?&3ot`j#sww9+6iqDy7MDMcQ{c;* z;@w1=%4_yAzVtYfcPeInThXmqK5$rgjM!&Vm4A1#AT6c~RtminfMK^l%70Nl70-WD zzK;I~%BRU(7?7I&tCqW-H*+ipMc1DRZVugeBm*L5ybZHyug(W5hUHr`G6S~#*bk`l zq|9O%%s|#$nk^QSqtUq@@WkN>z<&gJg&&SGxWH@sKa$`fcO(0Ue-0eB{`8b)X%5}o zPrXzweZGGQk3ku*)dI*Yvd`i46Fm4%pOHUxUv7a0tR$tRu&=4G4t3zbp`@}ssbk~D zkV4F-X-D9w1hYVP`mHh=67K>LYvtaKfC1Cz?9c){7OuABROn3m|oJPP(?~IH^a{Vj? zI9`MwqA;3bzm%0wgfm1<2*!^a?d~fn8^h|^bPRSMF#_tK&zlSdLdEof!^N1ZxH$ub z6<)1-f7on%o3x6oM~n6EX;e#Ax9p?pgwI!!s}3t=i0E~Kt01eS0lsymM7?a})Ay6l zD?RdY!6Jhp@FYYt*&@64dXJQ|L~=g$k)u~N0DLi@@j1~-?t_{~uhD?*USQUK)m@uq zlDUQV*!A(7P5dP^C+#)w#Vtw;YsZVm3^vL4F##gGeXCsRoE7Kuf%=!ec=hMRV!lxO4h7s-h$vgJkF|# zxQT^8`3WY_Q3*mHK>BqrJiCIucv-|7B)n6Eo%yoi7xms#Uw%|rRXX(q0K_(k!q%HQ z8R%cSPIl@+U2rEk?bPiTn7Qq%Xj}^}X>2>@1$&uBjuKl>B+M9$#nu`Mtp)5>zue`n zsNT`^9v3T3%A4=bbqRIBFQ5^c?7|mdhc1dZd z-$B(tQF`sHt@ueH$V@EkXHF9xfXXghrm6aQRK1}9BmEwR{1;)s&{}vu2fK!mi&OBn zGJvt_*5Yx^&M+HmY?;i{0ek6IP@p8g^QL3QCs*yD`I<|mXLokt!m*3Ur$DUsG0y_k zshKcs-4JG43E_Cr zPwGizx}2W~kMA3;b$?4ppvRec0-hyTgH*EoKvmKU51GW>X<*GKLCS}?i&6x>ElG%U zqzV)xph2{*`s$H=mqT;~&?fAJH}n`kF4X`mikywW^OX=qouR6yf1Y?~*eO9iSjAyz z6!nC2Iw9i^pHOH-#O+Pp@&4R^58Uga5fKk7EWwq8EwP~*67KBrKL>@LM}5yTLt?{P z&Gp;y9d(Ib?9WFv98}(yu9i7YrD9)0-U~tW4aBa`t}a`sj#-dZjHZoe3%&ptp)_;3 zJV*JfB9ws$VRhURXOFWDG2H-_x0!lg@;K*BDyK|T!Q>z#H~7gc9AKC!I1JD8k0-4a zVVn(%?k8H2$!)<@l-1zr=@pBOM7y=BtNkC6Pwm*cgPSToD%JZS2$;BDXx1y1Ho0h= z?He>0MV+I_EihV6W6!||eBYjT)#Vw>kF049Yru;%;L_E7hnu^EoJ}7X{;}usP1`u2 z<_>?M%L@!yb-O}8rYUOKKj$1MUW;uubcVbmFMaxu#w zXHxJVEKzKuuw#+kV~@HSy`?z2?d+3tPm^Q^b&q?;(jpbdKm4NDejHDeb|y8`SvVd( ze+VnRqhlZxVD|2l?SYn(Zu+=2!NjVQkQDQPk7oDLK|t1}k8d)A=8IYq3~UPz_Px+McjZv?nzqewTprt?eQ(W^ zN6Dx&+f}{>ducp{P6Y0}8sg^NdT*;IK4}my>_V!Q(sJe!8QY0q2ZE!An3~@gOu`a~ z4SUPyxu5z_jP{u?%hDq`fjwY)9gI5jNNaaKNK^aR-BTt}c+jVF7(2RTFJ&lzMy>$? zjlZKUg#5L1G$sAe3cUm8?#AB69yNPM_`<+?yR}>dVml+HKL{YDk| zuoPS+nprqQ)(Q6gei))P+Ok7qW@hze+qBe@1Jbiz){7x!jdKj#mkb_4MEg>Y-HKg& zS(T~ny;C9*zvg&zQ}_J(Hu8RRR8v(3-TU3{X2n+2@E~j}%|L$}F7U zMPqHoW~mzsrd$s&!iCKo+l=B3`lfUo(fma1A<%M1lowwgMHR9WKSU){;4*TN#6e3efu~C_{^r?q|HzUK?akZM(Q`?pJK-ON)d3l=Cl zKyKtF%XB55DPmM+l|Ej#o@iv0Wyx9U(jBsG#b?yJ) zV)bGC2?)a3H`A5FPf$;^8CmXsMX7fwbSTSsooqzW_%ZH+QJ+6A_+WbU8XFd|we_y- zy=^aL7T4y`SUfMbWuvrD#Z6G1f^U&8ww}w=|fhyx#OVG=b&knJ0V3g-dRMkv#P*{(}f^m%iJkb+aFw8%{a`yLt zw6c{b*W(9lSM>{JfnRME%#>T7^dtdV{^Y#~FZ+ZKW4$*X(Q50NGyf%2_? zGB37k=2dddyn6m(Ud5OP=^xAs|9{H7CdRFa-f-G36J%tz_9fL^Owv&^GG1)e@Xn08 zyqy?#`M&=W(bS`BAznPQRl@~QLE`*)F(pkIN^8L^@cXRnMV!@wo3?Gt-IAF=%`yg< zBroUc`cNVF`L?-cw$hnef{1X@+dC!5dNSuDok3keyD^>~K54L8Hz)MWc-G7IwvKoS za%8RzA=uUH^~H*SX(C7znvn*Uf7yFMav&qv-UHGsJ~onwTBS^FQjf9cJ$JAsh+R5A zP`YnlHu~Zpvx!d3IV1f*)BhRl z+WiOYGWaL#TK&(kYxghM<)HE3!Y(R`)Y4HFtM|4E19`~|_U7mA-As&CYncgaj2};6NFf-_WGPVQe}2tGy;f&g_Mc+mjQ;A0uZ0PRd3+vz!_phyopt>-q|4Y*%I z@0n9mZUl^1T##>QOoll1>1H}^S?ykmcPUGH%&_Ne%Zex!i=( zzP9jZIAk?roC#CWd<%0wC_%@ZT!bTkc(YDGULxsCoObXZ@1YnARiB9!p5cw=jI!i! z7AG6P-;}zfZeS9t(>UHS46Ujcx%^zR((c z@ElFzl#f| zvAjyt?($aSnvVN~s1s*lUKfngFMUaLG(~`enNt)zO?X8gTb5#+?+~xyoui0l{8$*)?@mP*v2IwS0c~Fe)y1uiVS^Typm^!7ZmAY;%?A zR(G}Gj9-R)s7DveF`5c`V(~Kr7PDO_Qj#=s&6a1v7Q8;O`$&3Ty3T~*m0IyPsStQ7 zz{|Y?rAue&L<>W!`dLK_Cze*DkjB8MUvab; zr``xQIt-K<=ZG<}DS%Yie`jJ$W2z6!)0>$uhzP8RZ_v=^5r`}U&F3gB%@CSG-GjwW z)zde$59Fl|GB7&)8it8p`yTTZ|MmHYS3b+s!=V3r=C#8F88}#Q41Q%3cg@a}Ew6Ez zih%p%mn-YvXG2UB!MMyC`zMXBaGDe`)ire^4xQm+xbk|Yj$H#PnC(G1#dQlUnDrb( z;sVkty004m@?H0ug}Tn_^>quyH*{`(&5HY~0s7z@%j9F3|En&zewQ#ad`()8beSvF zQAUqo8xsFf5pKx8xP56jWc>7=)tj4>;b9gRembaoV-^vI3FdBu&`#OjhYXZ1SRMeJ z7h3A@E{;9`>d|>*;PWh8ht=n%Khy8u=PmP6elRt;!70)o1Sdi0p_8fL@mZ}8C-=fN zh^IvJt4QIV{b6t#HoR(`4FH1l&~XmrFvn#kEhyliHFWxeYBC>2UNaHMv3kDKJ*aRo zS*(dO<{LflCjVed%Yw_Pghnr@9PBy~z+pYQxN&rcm~3-YuLAZ!brEr5ukhTX09%a8 zWxg>(`=i-L^yD=jP)56ySjAjrYga=^akFIlHJNgFiyF7o#}Yqf^-+pdhlXa%^VO0z zY#!ss?#%hpEZPX~%q`+w=uRDN7?W)9h`%kjc{!0fb38rh{F2IHVXzbTWSywj;bJ)4 z^P~ABhY*y{VA~zvo2_wq+C?V@mHsAwx2Bc77KYVfwoo+%i5s3fL1_yoVsf!6wJ@+S zTVq)zVivCB>zjNws6RXFudz@+%5CN9J{}~D~jjwLE9QxbZ&8t(0#cX&W%MZvs3aoV#La3ac4TN4aJ(^LStI75YC9H zHNVDOJHFI81;4YFNQE55@Ukl>gEd-QJOFns#VXe&_{jLC_iRFs%9hfP4 zpXk2073?<=&4OEvZT|3}u1y>Z=uSt%a;05<6P1GWTK>Ql#l##K$F6BpzyK3fy<5K@ z0H34*N-6lfeU1^bSe|IvT0yU07i-y^pgK%Gt{k|1X1TPM5H^l%21rdtOP*qHJK&ZD zDUF6Gp7YY#_NSE6*1Yje63fpaHZrn)De&GOXO=)sxtHHyqkl0M-DyYmyLa4giY8jd z@b2NJ6*u6Yy8gza{J9bHpcl`fyG94QW5}QX$;fsQuELuYxsr_jM#RBX zGn@v-&`qXc04?vl-&?`UTWi=(^AgF-(4+kKz~Pb@rDv1pBV5pO?>eiDr?BMW+2HYZ z+A@Sj#TNNX2*SKc*aL z#JsWV;8w7LA8S|3E&%=Qm|mJq3Z~(LIveJhIZ0;cp<5w$R$+l@)9U|{-fD&XUxaMS z8EJkP1imJ^ig4i%uO2pXke5W$dIfiWWd-vSg+t$%Fri$;t~2kSB9whov@L=#fJHBh zllmgebNl#h+X$^4TgNXS{3R(VD{`mjbkNsAnT~O|+{hZcYUvVBq#z6~P_`34ptQj! zuOQhvLirN>hs7t1h{GJgB>&dKbJVgFwF6ztF(>J`=uQt~HM1VN$dqDmsp`FAUHJoZ z>*H=`DubixPhNiETTyxzwic+J>9~-(A_QBNC{&H5 z^-LIegD0fF%A!zu1Z2v;NLRYgdu)bxhlQ+5D`Ivk2OE3w5AHyU$S zUpzlrs0f?bT`=V38@>;Dbrr%lDn<>AQ;ERGcL}z=`@X^xZ(=wZp{xjEP`RW%_+f?6 z1p@3n7~pbdPj+t%lWjiTw6n~A7mc1Y@j6*CBlBk3sH(2J)#A?mDCEdV%bb&UCbV$n zeCJ{Rj;_*6~h2kb0#H+8;c_9zXkFH3mOqVz;L85q8)u z`)EVC4*?~ep4JE`QB1#x{HMFTc2Y|Daq;3=ZI%f!TZcV|Fj-Hy`vHKPPVu>x@=twX zrc2svDdm_|l`DVxYwHqqHe;`=;fTVNoo{XG&60zn6*Up0IJmf{3GmaW8A{a=>U_|Z z=a0;s%wCwAFyCk-WHt z4bw|vd*w6j-@c1?Fpm7N0|{0NFXiL}f(R|A)n5`?frV!wqrKHTA(*IL{USVt zh!dL5Q|r_g)C)LTh%=c!S+1mEaALtQTYez)8R*4PSbOqBfZcJUf~Uk+5-^*&7s~sG zyZ^I;OH-iRq@D9<=B0yd+3yLhaAJe1_42wCTl)vx$Atz-Y&sQR)p;=1bW#j=yDFzx zaj*or*;)z*vGOAg%~x1fR*iS&W(3%2Hf3POC&j5lhHICNpCG{p+WksU`5{`;6iYp< z7sKEFT{f~*)T?>ROhs>A#AreL{mx+W`w=Ye$J{@a57pVKF;d*6>&n3l?0P&?4l-a7 zdcDy$K~^6 zI~xC&X<14t1ha``mVP%WL{g;zJ)Vo51kC1!| z(Sz1gUWLp+DCh?QuZ##?;}AgCo147;KGSp-(mL(aN65-J9khK`H=E1WtV3kIq1MK#9(9&;1wncgn_@$;kZTx9Mt+=|%<+b{_CxMzYjH;oE)rFJ6 zrbg&b6{HXl^v^m$7&Q16rV3QlO&X&n*WT$@mUgGiATs4ChwGJPnW)2=l;Q2miVM4j z=YC7|D!irlCt#`R)AzK()PkZIQolY5`noZDJ7Ykq+sHFz=<)HN*ff5mV>R8jNc&Vp zycCg-5>-5sc}lb5kaNnity^a z^7RrZ=V8*0qRTSozonlvzwN2S`sVn9c&>+yuM8CkwiQEK#R^+R5&ack47*+uRVWG< zGjQ}3A60-?8uhC)yo^~^5r$B(mR`_Y0`t zc*vNKEtB>LX2$DB9v9O%)J*=s;)_YEPS=jk>_@qINwfP$GowQ& z$3%Gxs_QUV4*WO$mgCOjikBs#8Q!*d*EBEnJ%ec0v&f1Ii+dno?`nut7vmAi=C5K_?(< z04vjCS2kPiK;2KMc2{O{@SAC2OY{O7{xT|!sxr#FE$7eqlI zl;rtm_kHXnjtvJ88CQS5VIB;lw4i)d7_nP$kCBBWU(0;Q64WEWc4Mz3lmBwHI;UTm zYBu$1B$~40QAab0bGA7mnLMR_TVk1|>Vu-e4xmCa3^p#w@p&=f074y~GpSMX8%4W^ znhCT2ezI*?tSgxvg_D(0$KSHwZBXhpztzktcy;#oz;3qJAz5%62i-n=a{F_rWjOc7 zNMl4=)wG9#na7(zbop}n)65hj3*1DpZ(_eVzhGiS|H93E(X$o672I3L%>N$#`BIjy zt(sU1@n`FdF`)wI1w@l&Dg3#_`$8qijM%P|kg6RS&~nvxo3GA`=et`-W33R@!BdoyI!IesXo7hG&Y-e(vGb zUH&A0*7^}x=X?3h!sE9e`1b?>YX<`^M;I@Z>7!c#oseY!3x5#}p?ydn&vR=rH6Lw+ z^P7Kr6#oOYC56|$WVnCjW&3Pub{+f5J|G@KfZU~%wFv?Pf z9MR4^py|hu#{oEXm!@mNKnljBr)&C)!U3?tvpl**y#f5E$ zw_fh^r&TGK87}Kic?*SAJ>=&UBCI4`?^ELFnpsuu5VmUud&~*Jjtjn+PT(i^P)!jt zK18G)OILaZXYYmxcxg|Gc@nM`$m_d%(<(BDo=yfnoA#zDN$qaheIu|5S6DYFH4iK) z$iQ*=>aJGqp-4iFp`cM|vgZu4UsBXSx!G!A-bWbEUy9!Il7D#qp0uGFy{F+jwQa7@XR3NkT3FBd;t7GonLhn9^MwsUA zi4wuH2vX;aPnBhd?{`gW?}Cr>@C%ci7NZvxzBqlNlg!-De+rw=msIe)%z9V|?(@@| zwx73hF>iJN4Yx8b%^SHL9F{P}_X#T6R{_nY1u_B$qCQ9W%2gnm%GxQqrr?2I;?FL1)$>AX~B@UOba%m)TdEy(1x<5+0UzzkmoZHOISu~Fmn;=BrU7tq@qa7cp z9$PPTtRrkmo}Nwys6>GJ>`w@-6-18|zY%ZlEG$1YNoM=Y@7K2D>mW)-0Q%G?&maAZ z%T)5Xh{~(!20Mlm8WREuiD-o0D!5)>;xcTPkMw#KB>7esY{ z2+405v1_rfejTS3S;ghrexDy>;21aAgJ{lv`K*gQ#F!?0G;Nn0&xW=&a@xf%C0E#V z;g74vQ_HSJac1PVtvbH-TIyFkF+5ngGT0ikPY&Ye^LfPw1Ph8FR z|5*}o_4)rQPXT-8AK&CBz!z7SCRRV#V@TNT5rE!i-OU+-F?H8ln_-2Ax;MFv zG^TR)=V9yUU;FGo(_bOhvqg)#(VG+}{5tCHmd)JcMX7DqkLHOc@+Q+!{&+Rw|C7^S zetpeSGPfM!?a8f~?s0wczeLVeV>UdP{l=K1bn5T+Tj{wNuqTg&Z6^xWX5gf?W+`fa z=KweNF%|ZAX92j^yD}{?8+ZHts0uLq41p{_AmCR)B3EsR(H*1}`bnmTqW<@6YBSKu zt+?f^Mm^Du79@awF_l5C;%hoED9gIAajDhnYC|SC2GOk8s#7`eJs{ryo90Ez)uZ+< zCULDf_f+>{#hPe)yGz<&q0ya~5v$yqmG-O4T*bbZD=H5g{-hJI*&!9Efa0o9l$Rb& z#_dHq&aw7ip7*-%PCjId5mCm|$e7t9QXYRca$IfV1^#&*iy z&;DC|m(u?|95RIe*f@Bffg(9LA33*BpvYBjgagsSGtk<{!om0SE!IhzE3LuHHt;p; zUAMm4da4E&)=8dxtxTv-W!cC|P+Om5(X@}vH|0Hv@kbHPZ%YHbMI*xGtwidT&U~=< zx&M;%Ba7HhG-o?|N_-K%8;XA#DH3@jah)AjtzukTzeaN>-nzs{db&+250BML(ppjk zJkhdw>4&B^AiHS^g4lSW>t!kNPWF7m=e3iUo?^v9yRe~`%=C$;W`Pap<8FxNE5sZOIK8kCk@DKL ztky{m23maxIn&AIUw$t69$e?NeiN}$O5Nu8W$AMkZ`^BR*GDT0IyhGbaALiTG!TZ9jJv9~)}W`h#gWcs0G zm%FctJ=nZiB}O!u_erNTW(-=E+#ZI$vuKRthkAp{s0Ha{{-}VnN?Nk^M4DNXb)uhj z?FlP72Oa60$U5Lzbs-|3YeyN_kA!x!_)In_ZUE^|LZOO>l)XUDz+V zM+tY@Eowse8ZP)z$;E_MeySG-6J>Otu77i2lhuj(%a!42_bE~H1|=K1 z33(P-TtR@k6=t($^eCdzJC(1EdGr&ip%Y&wv9;PQivm;jF-UE%6uH7LSG!tnLX;Tv zMuKM3Z5%psGd;l8@26njDA|@Y^I!ImFt*JKDXU9=3Ugx;s@@bt4!Y1H0JJ>%d4g%u+BAf5KjbUHCZ@or7rv+d~qtj^2#KU+~pTbg|-ry#_J#@bjj($u&S3{cG za#iEG4yyn;QvsB72FdZ!J;!lMsfxIfy@B%9P^c3-J;Oxb?E2TEe%IkLH2xW@}@beyjK|O9;5HbRhwo z*xzeF|0BFix~31jCCF+C9oe9J|lOwk)d^EJp)(-cOnk&#&aGUrhGDfL`&{l4a+FttW>E3+jF=N2T=g z?H_zIE!Rf6H1nQ*np=WQpSTy?hVT-}xP?Bw=}`XDzdKaCaVC zNC;MDP^dcLQQZnI_=u^EqZ;m0Stkr)^qo}9CiFEGsAUgP!#he7IeLFs+vR}cVF{_W z3s3nG%h+y#r!E0C^Hiln&Vy0KiJ?uXH(S*1nJ%toDV_RrF-)zS^d&{v{0aqgqN01? z@}*jyOou*<5RH9&CZp+J1jVct zVYG(}$6;}H{VCvdwTOn%3FUxu{)ydTPeA>Tzud{~5(;fN<(Lg^a)DM_VI*O{g?C(f z&vwo}@OukxH^0{)ec`wfR^??bR8C_%C-XhQ^-$Y?bhYkeL!2rxi94;vcD~QJsy;IR zT}lnjv15!(iZ?3E5M#J#&-Q1WP{M!HfMxu~yGEY># zpxW$1tBVqnNFRHP1e?aiC;7cF!OBB?0=9=-7Gp9r#r95BF;&)t`@jxG2??B}PN`0e zdkLd>FsCZDYoP>@FcP@ERd-CvYR&kLy9TgpGB#`tGA(0SAJ3Z6^jpb?wJ9!o*fJ=| zGaY0nZ zCkvC)hXes>2`zGmcIc}t2%RST&eHxDbMG0~)cSAxuB9RZ(wlTa=}mekN)ZV)fb=3L zz4s2H6afXnP^1b-Qy?O}tMuLpy<>pTODF+yCRmnh@BcpczBuQ8?pp+tnVC7~^Zdpb z-*Njk6Elb~>U7@K<2^;ZVL;2k>W_OuM$2Nl z;R9>KFzf6C>~n6!+!w@s(bF)_*EMZ$xpwaR9}gvw$j;$96Wg)_%o;_qZarRkL|bR{ z{%$%O$&)~(map06pv9FLVs;(lnKYcz?tkB%qNa-hJ>=>4s!fM4Cr z?}Hurns{zGS3^IDr@ttxTNKKY)NL~{eLDQfQ~ObeMk(eg{yOWV6PyL@ zs8*COGvh=zPA~!+Q%};;AR29XW*cod4TPa}e2#3#^si@cmk*$y0|xdyu}>%CQQ*?+ z;qXU!6g=I#BfLRaY9KfyV9Wx zybwP6({eB^heFgid;70RHe+x0?FfP{9Os!dk|!_Fmk>~vOs_YZ>rWn%2gB6bdGFOM z|F!RCiJXST4YD-CoO5lsA+jr`0(J&d%QS3vBdVoHA;CyjeAfFXi9)*vg1e0E~I^;f2C`MzW3;UL~QiL=WaVX@j4T2oL9mpXY$xB z1=FpEvet~?)sN@mzs_#mVfPfv&gYs8cz4)amDRn{rwYfh6b_TEi22?RN*Sj6TQw^>e4LR2W~`RsmnKj{iC@3 zS5(5>7eTf(%r6oMg;A^AQ%MVj-R~r17Fgq*yB12qo$KmRu{Nao(baurA1`59x3g{J z%O3Etjtf&Vaf)r-5Q3(CdRLl5;?Md4lEzbR=|Vr*YUHGiXk@M%mJeA1XMR`K@^9M(dHO-u7?TdGk*z2 zH~&mJ>G4N+v=Ec)RiMQxWvfw6BqcIXW-S-nc5Z|1Zn?#^s__w;sH}WbK6Hcd`)nx1 zSH^N{V8<;LW;18gb5B1QLe{@Y7&(!ig`W*%Pi+h~k5PM1unQeGFA^gQ<@Gy!%{yf@ z`9X@ishO-fy_l;s^%OG=Gu`E;5pz(tiwV_s^D(?=_Re@@dbD0KjHMaV{P79Y%-HYm z)4ivbr*XBoC(R}FwLa6qkEf#?SISxZFmGvm>q1Z6HtGAEb(y3n~DlD#45SE;zhw24zk!4>=RZyE7 zVz(w^0KHaV*_{8u<4F3>r^q(pgEhFj>m|IloQ*KkjCCDE4m}^t#U=?3Q_LBFK>MM}!7zOD|HBJ~> zt-wcktwJ|D%%2Di3?F6v8*s|&IZEls10!37;kj7ulb0?O2Bly^ytOHdN>8z2 zJ+uT@F}(XLtTZ|6gqs@-)+mef(UlZ7PIsFlvxk*6X!M9aCZXWN90m_AIH z@obtf=4#k~VL2f@`aA(vMj?}50O~Y)POpbCB`)=mKdp_kgvl#e^wlS5Swki~ z4!#H5iC~G@r@=eTRdB1Na@H9%*;M2eYjlk-v=f)N9)ceNu@k|Rp!V0*{z*2&{uP!D zA;~z|?jUJ!Wou*Yw?rCaBz{l9Vnj_t#vB478dnm)Xb08QW%K zAPU^%Fm@`6Tf4pFDK=#8ZqF%e-XQfsor57EG4@$)JT)YpX5ZZ>_0egTXhvyo?gbGg zjqZYo0zD_9T>rZw#4s8LR&;w5z_J);Zl$EKOY>+f|h*LZqgzqKqippJ(p{Q5u{0C;r#i0LBBU)~!%? z0-1e*CBG7c3O{PL5@nIdV9XE{qvY`p7!BwrrbNy*NXfhPEv=;h7!>PxSQ!i8UlU#> z5bZDfc)ehs-)S3U1(Dp3q*kT3=BFM}|bqZQ?T#Y8mDgJ=by zdR@#+N&6$ zX-evbhE*{T^QVFS{gyMIOV0;_YIM5&$znE-(wQ#|@yu}Zyu%XWoUM+{x${l)bO z1V6mh&dHsOiPv_HIKA)nB*^Z0iL`*#;S*7a`*q=N~2&w-SnU!y*_AXwQxoeLi zRLgSMUS+)m``KHt(Ve|S^q~PN7)FE3{m2K= z)eh&X@Sh{@zq09J;B-Z3WWC|LjJ5a&b5Svi`*TbIFC#giEuS!G8GX#%ug@67{3izW z3rsAvHV>HBdix1x@D)4*BQ5+Bw$Aj&k6Au~luHS1r32jqpoX|r(a_&Rj?I}5O9s+~ zpr4zOvod+>&vVI?Qit2>#LA`tUx#f#{Y#**Qu9hpx(=lDK$Hn#=Ul>=7v>%)z`1EK zJmI9#)*YC3h_lDhz9*$k~% zWQ-|CuzjvT-<+6(Y$3beWUihG#%bQgeN?X#BI0xKR{h4@@+&p1RAFMDrM~i!mM=sW z4DMFW4_LW~z~|AGv*Mr871iF?HQB)$R8OGq?S|IMGyFZpaOECtVZD2&PHlTS{Tc>0 zcFeprvs2@dcp@w8zb3JW!kK5ZzWqcG_mK^U?5mtJr;-GYqiicuByFM< z_?|lQUP--G@bVpm7Bk3SO8Jsu<)EE6X`;Re@+uS zdkZU&$t=9)0lSF9{1s{8Wu5n}5T9vTX!d~z6G8n<70POc zAvmhNj*r%k>$-UCLSPp)UbeJNtL?iRRb?!t9j32knbzb-(@DTm3b(SVZY!6jmOC-3 zh-k@O%g&$x@IS|->rkCM#D|d3fyCA5K50X?xOIOJZLq#gt~Yz8k3Z?4DMVuC82RqN zEATH#Q+fGNuT&C#sgiJd9tDRsLJ*%LMu-B;EY=UHH&AsWyHR&@pS~b;I3*M9JzqjE zy8=AymP0pN${Pxo_Pgy5x{QrAt6gMmej#nfvI2AhFJ#j#q$~}??`3{Wqr*piu1W-6 zt^jvs$w#7x;-)grBlDiCMEc%ZrTCOxnF1P*n;jRu-{*k+h^hH0z2EOCNKkm-?iUQ- z%4Q%STb*VIU)#%mLfL3%dml6Hd57;-+GacUXo0I@Tpm)dNlrm2!=ttq;dMc~VXh-6 zicYDkP;~q3S8}jgwjEwRNHG{w^bj}Ohq6G2L%BLbFz5JIaNYw2aI#@gyHzH6H_9Z! zDn5NJIiTp_X0h!JifFX3S%eilKB zy`sq-K9V}`A46xeQ(K*X=5m_M4VOS92U639p>GkYc4`!)RcVJ>%s>m^&aUUs8f!23Ur?37%8 zESW1fU`wJZI1$MHGM-mfJA=B^*0deR;g(5qrnGld`q{!M>5X-+`{6!KZ8^TDlZh1# zqk+R?SwgTXpPEUX@?|&>e_H}%=SdGel{EFm0)oKzTs#OUPor3T^@KP$i~o3ZFK^XWWX;1oQWNRV z0xtg5_-_yfTc`e6%PcP;an|}{I<7uI+TMiW

j#sfA;>g3Jls+oELs>#=(Je9Uf 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 Ns* 插件的编译、安装可以参考文档: [idea.md](../../dev-guide/ide/idea.md) -## DSL语法格式 +## DSL 语法格式 -XLang DSL采用XML格式,根节点上必须通过x:schema属性来指定所对应的xdef元模型,例如 +XLang DSL 采用 XML 格式,根节点上必须通过 `x:schema` 属性来指定所对应的 xdef 元模型,例如 ```xml - ``` ## 语法提示 -输入标签名、属性名、属性值的时候,会弹出xdef中定义的相关信息。 +输入标签名、属性名、属性值的时候,会弹出 xdef 中定义的相关信息。 ![idea-completion](idea-completion.jpg) +![idea-completion-1](idea-completion-1.jpg) + +![idea-completion-2](idea-completion-2.jpg) + ## 语法检查 -插件会根据xdef定义检查标签名、属性名以及属性值的格式。不符合要求的语法元素会被增加Error标记。 +插件会根据 xdef 定义检查标签名、属性名以及属性值的格式。不符合要求的语法元素会被增加 Error 标记。 ![idea-check](idea-check.jpg) +![idea-check-1](idea-check-1.jpg) + ## 快速文档 -鼠标悬停在标签名、属性名以及属性值上时,会显示xdef文件中定义的文档 +鼠标悬停在标签名、属性名以及属性值上时,会显示 xdef 文件中定义的文档 + ![idea-quick-doc](idea-quick-doc.jpg) +![idea-quick-doc-1](idea-quick-doc-1.jpg) + ## 路径链接 -鼠标悬停在路径格式的属性值上,同时按CTRL键,会提示跳转到路径所对应的文件。 -对于XPL模板标签,则提示跳转到标签库的定义处。 +鼠标悬停在路径格式的属性值上,同时按 CTRL 键,会提示跳转到路径所对应的文件。 +对于 XPL 模板标签,则提示跳转到标签库的定义处。 + ![idea-link](idea-link.png) -## DSL文档格式增强 +![idea-link-1](idea-link-1.png) + +## XScript 代码高亮 + +`` 标签内的 XScript 代码高亮、代码文档、代码跳转、代码补全等。 + +![idea-xscript](idea-xscript.png) + +![idea-xscript-1](idea-xscript-1.png) + +![idea-xscript-2](idea-xscript-2.png) + +![idea-xscript-3](idea-xscript-3.png) + +## DSL 文档格式增强 在 DSL 中的文档内容,推荐采用如下形式: - - - + +``` -* 文档最开始的 [xxx] 表示标签或属性名称为 xxx; -* 其余行开头的 > (含一个空格)可仅用于多级列表开头,以避免因行首空白被移除而无法正确渲染 markdown 多级列表的问题; +- 文档最开始的 `[xxx]` 表示标签或属性名称为 `xxx`; +- 其余行开头的 `> `(含一个空格)可仅用于多级列表开头,以避免因行首空白被移除而无法正确渲染 markdown 多级列表的问题; 节点文档渲染结果: + ![](node-doc.png) 属性文档渲染结果: + ![](attr-doc.png) 为了避免恶意链接,markdown 中的链接和图片的地址均完整显示,以方便用户确认链接是否可信: + ![](link-ref.png) ## 断点调试 @@ -79,15 +110,16 @@ XLang DSL采用XML格式,根节点上必须通过x:schema属性来指定所对 ![](idea-runner2.png) -在XScript脚本或者Xpl模板片段中可以增加断点。 -插件增加了一个与Run和Debug指令平级的执行器XLangDebug,通过它启动后会同时启动Java调试器和启动XLang脚本语言调试器。 +在 XScript 脚本或者 Xpl 模板片段中可以增加断点。 +插件增加了一个与 `Run` 和 `Debug` 指令平级的执行器 `XLangDebug`,通过它启动后会同时启动 Java 调试器和启动 XLang 脚本语言调试器。 ![idea-executor](idea-executor.png) + ![idea-test-executor](idea-test-executor.png) ![xlang-debugger](xlang-debugger.png) -为了调试XLang,需要引入nop-xlang-debugger模块 +为了调试 XLang,需要引入 `nop-xlang-debugger` 模块 ```xml diff --git a/docs/user-guide/idea/idea-quick-doc-1.jpg b/docs/user-guide/idea/idea-quick-doc-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..30a6302940321e4ffe283d82226558eceb91d2b2 GIT binary patch literal 42240 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∨~Q&RI&RxgT6b)(KFTC?t)4GOPw$Sa%13W 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 literal 60121 zcmdRVWmua{yKS8U#fw{k;#Putp*X?ai+(}h&^~v5siLh{1o{a)j>wb`N0DM+TTA9WmR7v zK6tQWCo3VQ?ryl(a;f)HD;s)yNAnbGp!2-4w{vKqRcOTJ9j_o-^ygP0q;^E{1uBl6 z-WVKGIrYp3zX)6pfB4oAmkbOg$N#gY~Z>x zURdN@G?YK2datDI{`xeezre3sGx#Rqu9@fRD{@zrreO3D&F%8wYFe+Mv46J$CHoE z&B&Bs5fO^2c;yAp50cEcBg?A^mN_sp2} zJU-KjVT%&~1ru-js3e6XtQ=#jvW~%%^ou)3c0ta2bw6GUC8cjvbLo^%=(XXp;R|u*Juvp#<0;Lc5H~VM+xZk7 znri*VOyKDT>q^hLVwP@f%x2aK`H`nQs;cV&s+oh`8R?GBIYM+41}pRW<+dApL+3z`YJTVz`LJS1*mXQamq z$FZX)rY~uMX-$wbA^}%aUU;PG&7y)n(PH@BmCb@@W$exS*KL<*yP==-G$53e(Bq6w zB454;Hb=w6i*7!ENRao!fa6L<9dKY*dE}f8)63~;T~9zjxM}@OpICEM0XB!dd$X0h z|0axqg+1zwQ{NC0Cz>yKZ;e`ANosYe7S8nbF%Q)k>iNCfoVi+$%7%7nUAk!QgQ=ve z#WK-Q1sj)Sch4)PnDnGqEBMftpKkT2TA>FM;IKGz8DrrrOW|wMI$6Z%4hc>_9 zYDTr*Szmlyb>&@TmLJ2)tVNq^n)*iFZb+#`7rR}4Q*xpFeaSF!yM`@qCnRQ@SvwxC z%R{xqS|ihM_~pwYjIYt|3Ca~_c3TlHs@O;6_;o67-=~wGuvSNA2lyJ!)%Lb$Bf3H0 zkV7GaeM|=5$pgj6C}W#bE}pM=Et4mAxI|LNzV3Ob0|*ugy&Qx$O~ojrw7A%THI;c- zlpUNDD$`pgSu^2x`t^aLlfFoQi9!LB9HM`%>Y=$nwPW&@p_lVm#95*v-cwzIt4SYp zk;M<2=j(TI`wECeEB)KF z?yNq(RgA9GJ~<4=Ganbr@%3t_XYQ6=+jx~hQA|rs0n*}f6blsXqb-6J&i8Lh=Y{-) zgAY6g)O6{9wzO}qbBP*K`a``bLkp%~+mY<$yywiE5IY~4_ zQ@3`|#$7bN8Gmcf=owt^4RH;OF61d5nA2-!rXMA^ZX~cyn{%Ssgfbj=D<4!E&*M!Y zmIs~r1gi1;J)?ik4+gKyE_R3Jf$XrJbY8Z<^-=3^3OwBJ7)p=3<@1zp{_Tdpy9*gtDXJt`cAWLKxw*HQZzB~ebsT}{^=&S%Ym+T z>}=~CI=aKA?wu+uz%drLeru>=^V~g0ZFOs~20G&Xa7wXwbD4>j!YFCO=kLRLT6>)w z713DN(aY(9@UV++;nw@G5Z@>8B#WlxU|t!e!6-qz4iAgqL2F?+yGQfPt)vJ|?i*em zlli1;U0$sW-$hnf5gBi`ko1$6p{ibH;-R<{(Wl2!>RMXVk}zR2o-AN6-2v``KOt0M zi#`0zXMlm?hpOQ{oOj^%csKNsgkY@(?y&ZgZlEWjo+;>aob4#jBxqCs#WY+e~GFawhsdRw#X_Gn!bF}LJ_ zrB=s^a;4(DjOOMmIg0#xD#Gu$ZBv_tyz+JBvDtpKdO5WkRJ`pqLPD&RtaJXaP)3Oh z=Gazz>s}V75PHc0(OBSN>vPO8dHWd~&HM*;;_=+ZD`Hsz&{xI9hQb?xH7UWFA#Fav z&)XWFpjRhiAH8MRA>3ZF6laGLo*7wCw%oz89h4;#X(1Ce8Ehh6(blb*<5#? zWB#RBOGdk2{Kdk{0VtMf*0~VT;Jp-bt%iGFk}#qx?DiJI=sVxEC46nW<*>PK8fN zh(-5hnFw)-s7qo*p^6xWU1mz^_SBm@&+6yFc=i;+5(g4xpJ~mClWz; z*_<$C$UCgO>K8_Gp+2=QjmUY7%^f4=Qs4MK85`8&&^tT^Q5s@;M91sq+QCIOJj5ZH zb?4Fy2zZ+o4_soO?SnqeE4#I2k*y_slMk4(MwPd! z2t7P|V{VPuZRQV?YuTX6Dji_!!39s|ZKoW@_HM;p<0G0LDNN_;v%EA;W*v4a1_F8I z)<2wG8Imy^>1y#gsOTFp7Nx9uciy}a26&U|vEqc-k#yW1?bd(V6*=eW5$CXNXH^21 z&ibXA%nV{#&9B62i`3JuGzJc8sEHcpY&tg@gTNFUHs-ut$N33rjLo5y_&RGEHRif7 zzdc!ted=$Yh6$ilQ-#+P zxeCIis$k4WV7R$Cg~9M+SttIaszM~Ro5IvE@d@)RIqUAh_qTyiY<>$5gf99*l6>X&PU<_Qa;|q(ON_Y| zOow;4WDo<0#rEaqB7oc|L{1Nn~mXX7lN=ZCwUNC zHu)z-Fr5bjhUU$&7o$%dH{s=%7vv+@F~BLkqblrQWjwDvONytiWs~mHr_!@H&l!;*QPgYc2P9eEtEV}91r79HfnMQ1zXyQ9y2W^ocAAE z_cu3^=}d8j93I%q(c48iy@=R6i&P2*nd6g?bhXy(b}3Y;?d3?TqoudduIyM#%qx^9 zTItGLJjnZ$@8K=2w4brXv|%Dq5rxq6>6wMBE+{4%td6 zBwQG9hFnvZ*a7zW3|me&%g8~itK1$U`J_q7+r~qhpWd*7m(?GV`oA2zMvfu<`WXH^ektU_l^42y6;6(!z+$-v!~=k9uD z0@Q`5%$~#Ov3JkmJf$6RJyb7Bn!;#WQs2uuWujYV>(yZ6?K{cJcJfn|QRWO(kvhy; zy$H26YpmG{8uTWlmz;jL0JqGY8`?RjGD(%!&TDB?q7qj7f=K@L=FgUHLj@<*rfVxl zTUm1hOblXWsa2Vk&8;P5gIdr7nQbEHY-|!NvxDQ(gVAZuk9y1CH72>D=z$dWpS^Hm zfW?&uN|0pZ-1eFlQSf;HK$xGC3Pq}s{IZGvO5AYMNr!k&Cr~Jsm)^&oS-OYRMJE+) zXqWlqGsW?8U6rD9a~2a!ARdiLlN@{=?~@N~>pLP5&V*qM44l5PU~ZyH60HH&0a0cF zteqAEM`M^&pX^qinC{fEg;evzV5A-l#ID%I}v<$Z{NoWW0JhV?~x=xf1o0 zlU8B9ekEb2LQ`KqJ~EE){?ZJh2N8}9G%FoARi{gM(*dlhX8rD(Io?aC*1#`kQbOQ} zXD8*XkfsZ^c7t%pOmQox*~f_*#^yCG;^kbn$Ll0-6I+2XFT5(7pGXuW5Yju>R+5?^ zLB0V*(q|kEg5rU@8>$&)j<8{@Q`_Gtv4baKLhCIRyw#yIqz z=s`GHpn(n()DW@7s?D7)uszR6SO_+!`T% zoC8(2kCL+?AX)2DwswkTr1*JEXWKuCC78|!0)0C1ENtz+&I9x_HUYn%)tG%Bx$u zCMm=N1;+%eC3TUsGA<(ut z>s6|7dl);AjXM^bwj#@OTs#d>po1C|Ei@Thqsl;5PaL3do5eHjEX#+PCxG<&C(w19 zo}7a!Vrhc!J<^Ev&48N$wQ?2evB*J1vepL#mkJ$X=wXEQ9l?1US(3MhzNo4-RZpbD zFV^=W;E7ZQCu5SKA<{=X&rWT4bKY`>_9fUy8LRZ_&N9>%OF}(Ou3vdgnlUj{uui)f z=`R)g7s!sYME~qQ#)AL4Yl+vceCyn7|CREQ?=-rOFdOB0{z%SAq%nPvxj-v^@Xg?c z@F#56*<DPqa+208_vTQ&7roY%XzjmPBs* zn3JwSUFTkrno<)`dcrt8ZP0U_F2RaUM3cI5bPPlRojQYY+H1t@o#^pc@3M-q+#^)K&%u1_uF|FT;;A zKBzw5bVDuk&tBw-kUQFhfv_OLj5o=8@?=H6tE(0q)Kj^zQ{|upEZ41$SX&tT9*dI zZkz;ld5yk}reVC&vhEqTC6KF)>P6qDj*T(A+i8o&^Z3XttK?+m3e)6z=$5F}NlAYW zg=og+o!{IM$pDtrNovwhbIL-4o&y8EFD-aW?Os@eDKi=v1Jw*_fyLg)+Zty=zynA` z4E_JY36V#C(d`h1bliSM$>|+~Q z$XBaD7W5$N(`HZ);?^lQ-lb5NhVD%(@TWVw-81$HXE8`(0^R9Hx@9LH3y=Fa1-yZ) zo#bynjv6Rwvnb?K7dx%!_bH1OAWEnEPOMeq1!siZUM{prY)!d6Ad_{m7u7Z${Yhdr z2B6Va8y!)!>db-^J21gIRf)-S>248`J<%vP2@g#!`_7F-8tQ|AX#m@bRcYJ7LZ8a)9%~EUm*3*|?4!t{4WmBmC@W@F&?M0hGz>;S4 zc}?OQa9r~Nx4o0IdWY)!MHc%pZidP2a~?Gr z7v8*asN6~V6PcuJx^x}`grEFjjG6)ohr@pcJtPxwBX{eev|Mq6_Y+%P!a@yn=`F!_ zFE1xXsrQrw^4taQlZPj4_(|)c4=V@i*uk~vr84nSF+%3LO+e-LL zsGexOw?(|cEW_W{aen5RY$xp|d(EJ7cJwsBf zryg@#vbA$pvW5f!en33HQfMhxmh`-665GJ(RVj!e$NNqNf=UGoTHNc(0*d=<^ z+8OYYVpz1Fiz)#5ALNS(_W#{`RDtGaf4>rQiJayqV&+X>vy+iMY%d*7549*UdiXUOyT|M7MJOthSbDR<$nyFcwd~&P}>Ydj9|R` z^KF@ew||ELe41B|%btN)K6AiP?PP%c>G*Z^?@^ub82OK9e7IS%Pii{aJy9fMO}%$w z&`WZQJPlnfd6y|htSNC3RFZBoXBbi2y%qHL245!}GQ#BCsVjkz1w4dQBJ)iq-@Den zjZX%o%zj`mVm`(emS>eu*q+(6ZGGklG4X011hnrDv?OdlP|gN8-nCADnhzSZjB}DV-jbGu1ivJ){#;hjP((i{6Po<1 z`|?@ZMw>i(w=i2N4o-www4y)0(1(V*MO1Q zyJg#BjdLEIN>JZTBfDEs=2;6cHUZ-j{HBJjZd7HUSh^fe-*Akg`%Jgm?I&~V^On8> z>9g)1<1875UFW+Z4dExHSVU%6b{wlufjOx87BTb2D3@Wc*~guKutBy#KeE+edc-9SJ0Fh zpUj%nWbv`1Fc#{_4D)urP6CH>de@FCAl0QAi&lfj|Be3v{&0Tugc^3YHC?4{v-HJZ zvUJ~E{?n!*FOH;o=g$ z;xB%=&aF_ulT6K_sj|MPDuk~)Q;g@46cvn+`R#-JIpzwQ9r2~1j1FKoP z;k6soOT!~5!jBRyX@v+h6eOoPlmnL^e0UsPz_rRQ{@RGVKb7c2zy3q)^SN+q^o1sM z<}=M3mJXj9su|mcT$4@{*VI@Kts7287|sXIyA4OOFWC}ZIo7+>%^s~UXfS&I8St6{ zIL2~jHvV4;`NCt#T+MI&lOb_0mg1lDqbe1@1Q>+x15IDU z>0O3TbF0G;fx+U7LJgBsXF5;Y?`e~~@rZZZwq=Jh!Q1w%Mibi*2Doid+pR*+{o z9q&#+uK?+7RBcX&2xn}I`ID-b^U~M8hFk8Al zBznuGn1==C2&%u731~ByN*}7*6IRJy8J#w5?^K!5Wv6WLtd@VGE{HNDtPpDlKpWhStpRh zf7-7yx3;vJCX6Ow5m#D0yW|5He50gPiRs5QazP?yR9$SXv^vwjyI|!6p}+rmDm}{q zYcRj?7pl#<3>lk8UwadEXL@B>Q^rdND_l_6kuYKBfozq!QPX-DXFknbh`WYJ{E1>Q zBX4P7I3a@-Mbv4@dnC?`%%}g#H$zjjDU#H)Ki#s)MUDnUO3791G1+xI8oby6BN>+K zW{jyH21VY{Gu*ZbtZl;eW8WDCDY$I*)jQ|ma$mptzQxtkj;QcdedX<19ED5W`m#-A z_lIvJT4;+QYJAjtrXx`Osv8g7% zZEVZX&;B;(d8(cjo-wIPZ1M`-Q%+HTBpJbiof_FxH|wC2iFaCCQ7@Hl$Stq`mB*Kno$c@{kvvA>%~?k{FS<(N0q&6db}CA<{i zjqyf9CnhugfNrd`TvleVSVAZ9xwY*}wm9&o$wd)z2RgqMt9hgqV)0Bk6wT1gZLW;v2n@%qQ0LmaOlumJr{#1gDMw9j3xhp zEk6G@nDEcZ9`Ns==-*bT0*C?t3jZ(OZ@jcjGTEC`{&F|QuyFQw9M}LnWk_GzzlWmF zsW=~+r~D-TeX_YP22XWL%3$OG|H4@D4_N(NU_1u#4AR<9D^r^-M7H6h7&Iy!CMt6l z-7r#viz7?QJEX3s;Ulc_`6C+NKNwWz0vY{;aC9J2>Z#$_lm|xQ5drCT46qYCS|+?r zIAVFqkyIeZd$S_o8ELEUV(>k=ZBL*WQUl-kMvoth%ohI4j7$s?%Bm1+luz%(Wa5@I z8HUG}!!VGf7|y@2<~e(#Bde=B?d!{?{q2xLn(i~bq+e5;3w+$NURePqY@O?0^wnQ) zmC^s5hX9I3K#2q-r@Rq$BksGb-U)J4s0=XY$4wGb%ezM+RBiH(Jv)N%-qN{U#q%%+ zaz6bP(dZh?PvH*fe7Zcctr+1!g)L~j!)`0r&=&XGTvA^Vqv+M}c%04H=)`h~y%vVC z%W6#FZ5=+B?-)zMTixtf0!Q7kaxUhL9|)4;bN5Mbj73@Bc`6CFf=2w?ywoxEY=k^e z2$$D$>~6Nr_;+yiua%J~^XRYUPnux$cP2KT7y?QHB&n&|2qrYq$4u$);(l}vy-e61 z=`qOmJQ|ecoYd;M^v21_C0N~7!?`oz4{Mg-eO7W?dN5zdF28-yPKd&7&@9{tVm4NJ zQ)Up5N%lz4XCy54apZ*u5K@TJrW3!CM3D^SHMOZH^%c?xj%}$SY8sF9?c=kX8v$=c z%u&I#^c*ef325l&G*s^XyPrS<&-W$Tj@%okT6qKMf3WfE(<3`@)PQtD4bD2h>~p+I z7J8p!wAC(|X<|PBc}l&!3zi`nZ&n-2qG)uxZ^|Z(-*8NtM|6P*)-qZ zL7?A1hDRvZEjWwaP%yr0omLF}*%gv-4(m4LjFbNdI(}M)sQ;6zHP-i4?zCl$ft)0wi&c*KII3OiK6^#Ek`rwO>$1xv6ag+RVrn+l0xlL@x*5>|^3srk} z2HIbgxrUGLFF^H`dQVRXY))nTRdoW}!s>lyGFPAP#%z<)smp>e!+U;|UkK}mxSt^& zk+o3ypf?PiIAWSbN6)92csVuVwH2r?1PSNGQwsjwS!Np!_TVUXiM$e2f+|!U%R(6$ z{m*OPRTHMTvS#(ex?~gr7m@QD03iRZV^IalA2pRU^z!nNz3Q;(Sqe~Y6-e0RPST0~ zY}U9KmMNKLWGi#7t5soKG2Kc?b5^=vV+u+3$Q6_A>n>cDiYOUYHo|`L4wBA*bF?+v z_`EEGy;G)h4XXIhQOHkcrdu0COE8`@s~Z`ijd+Vwh+k02o&B0-(qS4L^m`D$p`Qt* z(an`1v(#0UGLc{dS-)4USy{qr9-me*Wl#KPGu=c_U%)bKp!BD7gfy*DXs<+U397~=`itOnO9oh0XhMG+ z#7W;#?o6ARt+Y={HqBmEedXBUBo=#E@5A`hi`!$c9r%1r!Av(aOxiU1DVFs5Yb(`k z>xw2u*#$+?sDJvoM)pIny*`Arj*u7 zbqu{my66Ib!2%j%V%qVf5$>A#RCb~O-TRM2Y+BHP(GpHW$tFG zvR!F#9^@f}3H{i4%yoSlIc`6IFd|l%(gyDKykfsQ9n%zV2ASL9GB!Y>nC1nRLVbmF zf~J%jkM{p0-YeYJ7eJ-AQh!$ws&})QN9j9o#x(;HyfZiHi59$b;|h%b`z&xhkVHx^ z4$Q2RnO|&r2v~P8`AH#BK+9cc@e*bI~h(@$^j6v#YM1&4UtOf!eJW zS#s~p6YRb!e<;nrxAk9-#TM_Kdywg-h9tejNs!L08gEUZmeD}U%=6`YpDO+JDZHm4 zn5n+@LkWAdd`|l!2S$PsQ7o&_ssCbAjl@m*GP}UKHK!TG<(QFJ{dSah9NBrChhKrdlFf3?t6ED4v0gQBC(w1yTFNdR~@B-PZUS~eVVQg=;jv@0exc! zBPu!H)vt=i1AX7tIi7ZEZ3Ifr&%~QIdd&)((tQU0vCwFa)_hqv@ABu>LP1P0@Ll0r zow7$OkvUs(8JLMZZrgoPHlf`8Wwl+$7p2l)lhcodh?*h?GRe9GOC&>3J1&z!oc7BP z&b_^edChND-5mDkRcy9P--_3J-oRW%y!)$hGeTI@Xo?0ULl80%uSLoTxN8PQ3QK>c z;?E${M|NBin~OVSn0BisCVa+v;hyTBp)L`0q2R#tJOh~TDeT3v)0uCgsCD4S2=h)Y zdT+-(npgcs(`~LVq_@v8Jm2Rn#Eq^0biP-#JwcuZaC;v{|^ zsP|eu5=z*<1c{;&*uyTffG9nu{nbY|Jay(C2T5f}hJofuifCdv!^`hiM2Kzskc3uZ7T^R_p42Wj;cLvo6kkay)mK||krG5;&2C4P_vf1ASdU`-vJP`OIvq6pPMsKa#q|8L8w--GRBm z|D&tf>wmswes&lK>ohN2$AUn>r261U?b8xI2YD7syg6y5TM~xRK_cAYpU^t29M*;P zV!7t0dhDpizAhvKV+KdV1wiK5ZHDi5ehse&^0DX48_^15DR$%KRgg{s$E$JO4~s96 zkK+g1sOyDCvr_M|YFfq4X4Pc7T}3ON^uP@v50ZR@oHNY}Err4<5i4i$b~l$^!cpvs zQfbl@N%&JOgxx-wlUek_=)qAn%S|XP7wcm5t$eDXr9iDONLzKewM4*e^^1Msvx^Ne zth-|sew&J;Epq*URUj}21X@C`cv2b0Yu6Uv!j}}0RL5jIDAL&U% z*%VovA3UN3_G?@)6)R+6HfDTdkAs!!h89~O;)Sbx6m`*(_oPq&@dR{-H-N4(Xa8>M z{KKhTvGlbr9xyR9WDK2>BAiQ)I7O0TYnJMXn-*TS*Rm= zK2qF$r};-hGMRb*SYT4~8z22Muz>;+JO_k&>RLXuni?%5|w}qu2hUCzdZe5%D3~W+BIOI}B~<**8gR zVO%@SFVq0&qg4H-TGx;2_+Gd654)`U64$!4Ja3u{!*w*MIV#v)tsJda`@R$dKi5ww zJ*JL!7Q4ib!qV?to|KCArpwM=e-VVjW+ES0z`4CgKP2>7Eyt9IR5%i2L!#_V(aht7 z(C){`;nRrz8)w8bHZLcl)Y-5W4;}&LDuml4&1f~+yZo}{^J%_=1-{_Nq$x4mI{686 z61kntUf7gT`kjLn!P$O7L32}VzdF-z3;IUXzvU|xv0!#NL5@=0pf$o26r?eg`wYUft&yf8@Rxu$-+1%a{WqpywkMMC7=;Hy`C z+zjCg(yO(c^`LtmHgW_1WA}X?*mbj-9M4La%>DGivpUa+<{;N5c=&3IA4c~gl-b-( zY|sO#+dX!J9xV3+s!dn*`>j3K(&lcCzMy@2YvFi*N4{4u6~If>)~N7ceL)_w{*Z1B zU8PnxhGLbxFuq_--)!BCdU9V1JMt-QWI<`++b~7H50Cm?KCZHjwp?uF67Mpv(lOao zs!q+qcZR&Loywk1`LknMHM)DcoFpv>{j?@r&PU1)2zSAr{~*V`Pk-@RtIjh8uz~G* zrX6_i+>0=2JgqrC6_Vb^InLK+!8nps2&gq6Wwk3q4y;m&wX4!N8{v4a?hf-|%=yW` zYA$XEA>*MJk=X&}IiOBWn;8>}>)@#tJDG!JQ{~G=S|MEC0O>ku2N$^F-n?i8GjH_! z;jE@bry;3XcX}k9y(s$Bx>rNaB!&8iRSpq?h#8z4}BQ!~8Ek9+5oNb`mcAt1ts7gO%67dJLj8;4)ycyX9G^NuI9SYs;mKoO~7#PB$kf$Bk7BB zdW?#2OtkHVXh^nC8s7GJf3?tJ$DPs}I#}71f}JY|_2QO0M#eC4eJ0Xwz6Tn}xtT2S zat3D{+)%37Y9`m!y;fjxrrwl@+135-Xg82w*PNTvYAPXf!vB5HEsv3I(|`;+&f80t zUe#+a=L0l76zw>joTuyz7rZ1lm7#I6>94mb=|kFeZh+!=71WZt-YXS`V;X6}K$hQr;@@+L!V~%jiSVzRk9-pK^yB&w_96SyM}~Qbqo_>2 zh}F6>{wM?=+k6EjJVd5x?qo>m#>2hmK9=MjRllA&T9W?m!9bJSfXnYPhDe5C9A1N1 zOZ>;g3ZX+%g*B)ePqUSYrrEwClVrUARD&9k;+~peI$q?ZxZp!@TugA2l0-9Nx13s| zYWP3pojP7eOt8HjT{7Xx)N%I=748$s{{ z9rwO4T<{{Y7`tnPioQ{Hx|uyh6tEB`7GiPir<$>sJ-{mvZ;av=qvD2fD|X{Iw;j&!E8aV3C1Dfr zzr*-#^YURu#>J$!P0Pe0h8F7|-SASrqvDz5{bLhP3amNc^d;#Nl~P$DFuC_uk!@(k z(3o{1w{J&D?tY9o5aIfGlGT2^(r30lD*J6=aS!1AIF)H-r1D;bf($MOUA(Rr9rr6( zd8ki-Wy=eWvhQ4W#DSOGe$@H4O_yAW{f@W$_^4C6Kp0C3j)gQ@R4*w3l$i(MwM=Vt z^+^NAbCtr+0m6fqMmF?%ie)%LKN=!Y;V@_+USG*5 zme4R>XFBrS$KK)EoR0q*=95Yb*aB&tHM(xEm|ejTKHu2kkS!H7X=@5PK+xQ@WqtyxeuWj;Cxc^RNop=wc? z{~0DaxISu~+LysDH88-HY~;G&A`<;#W4Q3oexV+oUkoLwX-)CJQ|g8*T~8eB$>n1B z*CB1&<~xVkq4iu(u0gySU>}$Jmzvfa8KJ$}emSb{(C)kHk|VW*ZE1v3PFwGpDkpUn zDHOilMfo1&7HeQNP(O`>)Eh_EIZ!LT^}nun4IQZ(ANn+lMTPm!3Qlg7ke=hcGW{*;CZj zYBsSGLTlCnnj0>Y{lTwIL%oP5v-Jf(Naj|lw$k2xBO|83YI%#izAC&zLMW|?ut_wH zp5j}RFw-hO?H2CxnQmO1t_`)9FmNX25EDG9sa;*LFn6;SsFQ4&0~J)^$Ho0t$WpLN zw%O=r**JH+-F11`%%{9kQ|j<5yZ2sIoqOB&x2NZS^qt3Q^8SbO+=a=Aavs-lp<|<{ zkg{y_gnxm-@~Jk<9MeE-JzEVj)Sek5?rMYQ_;f^Jcw~z!v28@7cWp)b5{o zGUL;Y^vA4*rIG=uc{a z>1Af2`O5UWEU-m{r?&UbikG=o@w6+uzlf8oRGC)~U{WCDY?&!AH|H$uD{57M&zMOK zc6hx6kHw}COv@+9n$I3V4B;p0jizjp?lbGn)(T*VbsG6@$}^nB+rd^f$n_#;=L*iE zV$yJ3Yuuj&v-_QFyZN>93e4D0Cot%l3J#j;UFHj);Vv+FV2m!%@=*<$cT@I#|@|guL(FY%H(HpDG1M&7|aNP$v9+ z1UMu1hCMe6X`#>OHOw5opj~lb&|KChiXv_1b22&x;d~gwDGL7%3m_bx&pz8cQnqovACrAywUuO!4MDO^v36R&Y<)O-+p)T`@-AeLVq?zXyR2js+KpkVUz6V9amL zKk31ff`$txoRVNnE+5%f>)zrJ^QXKO-zX*~t<1U_sbeK_H4Eay63s3$)LDe`$ToD+ z+<&5)3!Exfo;VnHiBMdgwP;41T4ar^M&_|UinG+1yzM&$L~wmX%cT74ZkLn5m756u z>SJA!@uD9!NYys)qffqy_ZxxEb2Kq-eCQ>9)pm>}&B7X;nFmPohcHVu(mqBiU?`fH zGxGy{Z%#bgSqz{k0+euWXPZpKT8eH@C1UwQtxvrad6Cw0WWC$EbB@q;9T`bIq*e~G`82;2B9WMNKW^*lRpE07ZY1MVp;l9&rFU<1&?7(D zj_|yhm2Fnb%)4%W9t7MqzHq-ykoNs44W2Y@9DoDgzxGvy82o$+#;k6({zLgzn)5GN ze^UMrJ>TM!tG9MUi)TG@dX#q0koFo(h1`H7uEfn@^8A=Or#g- zacAHVPb<+Cnok*BObqO*XQ)GgngJM^9D|_UG>p9DmkRhFlT9&DSQ$>Ye9Fulbs>b4 zjY?>1;g_#qK~cQpX0xMlS4U_nvZFfi&{1SC{yuQ}q6UcKC6a&X15jiAJujvu1ZS<& z`gr-D4D8(?j!FG7xQtPrml`h+nO&t5;sshZZ^f{8tceGd!oxOb^k)~M{LHAfQdT|H z_zxv`t|VyJzm*rErtcz_{chHVE~K?nmdSk@&1Mge6sv$rZXg$2Ie=~M&Cu&FYf77Y zuvYUcre_dsLk8Hv^4S{_-*4*%G_VR8_T+^IbevsBlY|16m*`-gbk6vS%o|f8P0+@X$`@nQH&}%Eb?j z7u@PCqvb-NC{jc}A=hTbtZ7_~mj4E9RP`_-~OD~KbQ%{HO;%%j~Gw4yiBJ4@} z*)46vLVu3x*d-fou2i>+N7`$=X2=a^_zR6PDJ3{btisC~S!jR&6ZhX&RwmX|tJr9T zMr-&scHAFJt}TAU2+wT~80Ijk$QkIiQgjuyDX;~L@n zn$lz>k65Fn^dK0NaEVLd2%fr`W(9Y+A47EXkV)Xsz}WAo|K!Gtg9!%H_sJ*Cxl}`~ z`{l3VXaEgDPJ2IAEU5S}_9uOgo=RaP6v?FMfHBLxx2vC(U$w1nEu`;@FB z_uu857qdy3&l2D7(`ct1c!)4Ley~BZl2eu}FvZW-Z|>eA9m4-obY3j}S4BtWA4LaU zs6xS==~(>z3J3jfU8i|Dfh+NSU^H+3!@la6@*?gOIFQoee(3bX$7l(Ri2>F;CR`OB z%zZWlzcILXa4?xkVjWP~E_hR%sqenDDp+|Pa!+aD3L_<-ofWTW3z6t$XXwq=rP(a)Dm#e&HI zl}!(8)(;lMECBbxn=^W&W+3}w1jQ!+9s>vWR`NpQ6h@ZV+^^W_(2`xU%zT^{cHJ0vQk&6D%lNE5T^r4We`~7rNhs zW!cJUdv*zeyd4vbe{$}{VQ2`cx>4js)~Ijq2Tv?YMZ?2yUL9--1_wDxwvKplg?We1 zduo#)tJq_=w|y4-Hs#xkU{-g@!#Soapf$O++snKXY2qYH;{Qe6TZcu}w{O3;AR^r* zEiI$8fRfVPB{3i^-Eboa(hUwN4bn9WC_?G{_WD6gySEEfroeC67Jy0q-7 zZbtm(OX*`ldL6Y4T3cICH%(MjFEDgOh&~3fL|`y0iQ`!%?X(CcGdEvpaF4Eg=Wti~ z4~f!zZOS}MEUwNC7fMuR1I2TX`=+YG;PBci&!Zcs=7YD0ZxdTAHnTAD-H%mKrOcOE zotpmb=n{mx()PHbS1W7C(f~VRLfiX8!g`4^9-Pj;-bC0=O<*wU#e7NzNdkaNRAXcL z=Pyf^TjH;cjeYDpjCsQJafr+=*N>nb^Ow8E^mOpF<>6aBEz~H6&JEgBVC@n*rHA6J z1u`*+T===#2kkFDD4H3P^z9;E;qV8z>*sD zMu+Xl?qa(|7_1G9+x|3Jhwl9Z-u|C&XD(Kny7x6a2_;_LeJJ*gm;E*=Y0V zKG$kWP0yt@n)QGVC_HSwCOM( z%ZV*YNIMUmsrN=pA#KZQrG34t)No4zDUIE+4`y5f^Ip||&%L@_jf^jNBPdw;wpS|O zt)o9d&8IXcW-$wS)jZNWVt;ct5pq;klo2 zRp(px1?tFIh0F+lo)lkS&MSOkdf7aD92Sf`>$T$3ls~F+ggHjmg67~7D z(jNFFm?n&E?Gwy6lMRznkViMJb!S1AfT%b)8MzX8EiUNsaPP8L7Qh&A-28WVA$`U3Je`3+Ia@2#Z5%>Nx|ftbf8Dy$MIL*x)lhU+*cR-k~@F zM0Vp!f(B>|XDsE!ecPdd)eqzD+bTrNHlY91?34i$z&}QhXb}R8typVx%4lP(>a**S zxxN9CHdZ0(R>G?1n;o~W)TShW+Vslos4BYs+Usb?8yH68hXf8wYM*70u~D!#_b*88 z&b!~1Ezf=|0fPynO0|%ys819#we!i+=3-(9;B2tPrpqfig3tfMz%sio7P4D_Ik9v9 z*<`HLi!gCisrL5c)4Cd{+g>sKFcT}iq&o9(QZ&m6Ac0G>lde!be=@kg19zPK{8Ka) zj`>Y0D-)A`aU^Zr?3HVAu`_8-~w0-!Q(AN~`8^b_j7a{U(0j#;55q;4w9uS%rv;-qN0ZVek#fl`2 zHzC8IA*<;>#c|uN^on2hEn80xda!>TKEwe~{5n=jvsmJSEW}=w+@+%&8X9(uj>Dzm zY4Z>si^osW^m|}_n=ueAA~xCoyvtRcS*7Fk?$iU0sq3rbh#ox%a%Z4D9aD6781;>5 zWkSe#F!3f>?`i&}NGq7}|TCJ@Xcg0LcKPDLa{78eslSkn<8ktjEpmF)Z zz2Qg1f~eVPjg929Wj8wDW|jIjuwjtUPh7&u@ZFLsLQ5%FPS$CjK;_FsarX0K%b(I6 zRQzog82m#{Imb5xPD&;gJk$WR@I;lsy?8q11gQwZHC=rbDx3&SBLI28iY2C;hPp-@ zWj~HAb7>sv;e%KeKQPq_oU!Z`ZAeMQaJ|C_kWb8C;sc8fKR;-`*>jIv!L&`!_cfq; z<6>W~eg}AIK?z3iYZ`kL*9%TGd@sy|ODAk4TK!trIRW+CiHTwsb&v6R?*jL&E5LbV z7yxD6iNuJ5Ev2p(xQ+x6C9Jf9{*Qjm{lM1+FJmy5BOi{?2uT_}C}kqiFJx|g@5^1n zOkBcn_X+fQTIrJ{;AmTcqwVW!{t#hxF7xdDtzSsr;thfi6QY`?Sh8Zlb;&Y6 zl29jQ?Z%_(f&{At6`t4cM4s?l35!RIs~>`Y6NEU%4L~AIUHUcBU`+xcAhb852&v^mKQj}%k<;^rG+A;{onEs^uo_3K{~wE(i8)0s+Q`&4F%}# zP`tW>Yi_ufJyC$a-7J}%%dsek!`2C!l9Bj`@ka0G+J1-RC(ZA;!YYz)0)D}W)+&@G z<8sthg^h2Bp#@pt>@Kchr6T)4Wsym}DSdsc`y9J}ittX}<|a#LE#2{|KU>P5vK-xl z=Q54i*4n3da#=fgDBf%n*l2F-k4-aIswLhw%5)>-(7s`qB{&!g>0@c8SS%syb=uIU zqQ9{rn3bY>=4uE^8lTFLr8zP%47|GC(g=<;cE?z%t$$b8qdy0c>04Vuhp5=bhQ2Ug zQxS)DY6u1eoNmRyr9~tB3^zwqIR^2V@U=l^$&ftlB_t7N`DAgtt3x~B8ih(|MP?l3(svosH2em2*tH1>Wfa6qRp zpjBX6^*2U;TrvFEIXi}2VA9e0j!eu^!^W}-9e8}TR2WLRca_&zR@4t^gce-dCV5(8 zBKGpkLYDbp_w%i0AfD4Zmr14)x+XrvJ4Io#MIfk(>B##0;0yKjuOS_Aj{~t*Ea;V0if{xhn>olP6nAs z3;1#EXmHp1J@x#M9FSW26T$m%h3z?G?5=~&X7gF0K#NyOwAaY@P0|L)0P~7en zN-S!1G{^CYCvhg$)5B~Je_H-kfONaYSjhP+vYPZ2vG||sD;%Hz(_*_zGd) z&i6Y2uf9^qWJmM|51r~U4&s?A-KP20~E@8|L?qx;t`F{Bs}9({@nOn z(ux0PUFHigjHVzHc|e-A?+!p9P?QM}wSDu?qY~->3Q0Pd0Q?Bx3$|^U`>l26-X&xVgQTtc=(Pzqn>0 zAq*XeDiusYed>;#L~Q?X-KTsrz^GGYELP{;5>TFDm+%0cZW8%$5MLRKQr5~n$AH4- zkp&MS&$#*Ep~~~Fzn~R*ky`g_MXLa?d7nZS^*^H(EB_m`Vy`BV#&YB%%9$zAxZ66s z*N6OkDcyF|U}1o-3WbfrVL^_*TOn?C++WeWHLU+^_b+uUCWw9)kZQM}A}VsOBuMFO zG?MBM2fO0sErEwowzCL0y=C~0l4sz=9w2fi8wa2Twgw*4w$m=|MxElAtFn@@380K4 z;#S)}SIi70ogIN0rua|hKW7D1igQ{2tXFrVgx~xL>OMcb)*=lqM6MYJqdP5xWv@Ri zt;9EstAVo-wOf+#;w0Jd8J(IjZ9g0_STw3-?9xljY4%xA)#jXz@NQpKr%Z)^lDS>ZYBh95ry3aK1+Q%- zT%4I*{bFfk0$9NoouuF`VY zeMu^=?k_#_G*z<8PW}16Ik+W<**)%7`#Zkd8J6^G{j$5}chd%k?*xtc=9eqe(6GL= zRb}WhH;UUK5s(FpwcG#__W4Y%_fHxj39|nQg z=0cF(jCt#8FZ>6F^!*edIo2_zC1-}dp>=;azmTQpBtUV6lvx{mFc=L?w0cx5;OBGRNOq16hE27Dl65?q->n#%REA5kHD7uAiZ7fdbT~cGUD;p{q`Br1^SIQvqPd z+#UtKLgZ$Wi`wC>47z20{Rel9eiaDc+poKDYt>=ckkGQy+Cv{+dvPW1pUVMd>V0s$ zFDbhWXI&(Y?^e&lfaym+--kJm`m31)+f?7A$bY)<2s9~!T+SxfI~3aFHlSPG3#Eu>3Fg^QsEiCU)M$ywXawP$;r zG|n)qAr3`urtwb;^xpr!L4H72v>Jc7mj@Jg*TAM??~`1^90#ynIzpMoNR5w=up06_ zyhm!vkHjJxu5b^z>7I2feE5WX?CCOGZUhlc*eO^B=+|0byC^r67O(dhV$FCyNAR>I zUfP*~SK&T}>WQllvT5vRSAE*oYwRu%RugxE-b_&hgVgqJcdMDY)nZ=FGK754X9Rbek)fOmO`q>lSlfM%3hcOH{n8reThy9;Q36D? z9-`tr)$;_#`UK_}yqGdZ^U2m!B#T0?_B|5@`K-E0SIgeX5w-JEPog;M7s>tr@pb(d z0uSWiwHVU{grybWq#9SW)@SYALZMXsPz!32G&}pzT>y}2$Wb=NsD8Ho{53DAGJ3gm z@RqlEwNCM2%Y<+h*^dV&Gl3?wJU=jj4mob3I z)!L9(l>gY1e`_&TQ@@1cn{u=_3dQ{wKrd(oe-4#Fon1aC$@t=2u@Q_4G-yMV1cq^( z-qXWOiys9hj4T^4V}dnMrNoAe;cmr%gbbVj+IkPNY=33{Zz32UNf zDs*EDybvf5mUG2_L}uRXgy9PB_K|hB$-vfuD8v_|j&e{nd20F{CA+%sf4=X9$RUM} zIv+e@ZR!@EI)QcC{-BlSZ)Ex}sg8ML!iNx9Z$hLi+67L%th-eGAmijYZ}2-+;) zOe7|29$huHcuMbD;6w#WXnc3u&yVem@^WP@*Soo>=FuOj!Os0>1;SZ7JNe>irg(m` zdd*uUHj}l${EPEBV8z+MSQ+rz-v#klfVA2#!4Ly}GC*jhSNj`=(@{Dd^*El@gC#Z8 zZc!u;B?HEBDrEuw21ZhINbMeB;lif(%^%>Jc_&Mp6Zm*@J7uNIOO~+8ZNXxHL6+vY z$FFca8P`%CcrRDxE_%6GGfh06A6N0tZ&6l0&?pgMx&6!TKdfAO4(zxZT?8O-YaZmL zfGeW#nu|Wh&MN+Nf<98uGPUm|M9g)o`vc03V}Q+PZm?!nx^$p(y2*ya2f+A{c7=+G z9;d|W0|zmhB(Z{yk~eOzFazrdUB;O2U2+UY+FbRIPB?o?+TB1;;Hb1!trRA2B z!%BlkwVLn7AC$S24d@Co2f74+SrVb(+tv_3>{8&(I7I5RZZQxJ=O(#AF66YYPBMW%@D$|8iO~Bk^gp;RoFp|7T+p%?h4NU z&)=k{D6h7pYM_mjEVsfl#l+|~1`La;zor>dO&6cmt{h1Ndgsm%=ej8=O8p*X#AUvD0r>fk;%L}-4FF2`_&eCAN8_Ka#1a{z~>=vzu;pq z%bada+)4Hdrb>~h?)I~hTSWSE_0O$-mwmX+|K5&EG!p^WNmP$tu@OLzCAht$pqj76 zwe!1MKJ<<@PK@IUh>xkKu+v}@#B!ujigLf5Vy-#)c?&D=lME-p_`p+}KH;n-IR25> zdv?erAHh3m@}2KC!IPde^VMNtY6P{1cb;9@<*(S!$f-9NSG#Ba!C}Y&7BW%$tdCGS z1sXDN7zBm@Be~8d@I~S%!;` zIDN%7#1M?x+lXA#C*`+6BM_YQ|CS`o?Rpto-!nCOJHe6lPI^02m*KZAr7E^2!3HN( zitULHWVILzJvsd6nFY{*I8r4^+$FDU>4bq|$=BQKKx9EcP0P#Mu>qig%4$-g zl6ViodO=@#M7*YF!t_c+>`NowAISvR6&31%rQs%V(zX6l$|eU;|EMT`qUul_PysAP zTZfLua}c#7detIYGeKVEF{qI4mK&2JPb5EfMhn4b#SVM@FXm&?LVw@3bC7;zDC!Gn zb5BkBOZX`XMD zkr9w~Ql*qTdm1e5?_ft=!x{ZpruGGpn&Jd@#<5MGI!-ObD4zI)o#gF0Cc1hXOD@gP z1td+fbi4c>ZD;%iFW6O_qYI?Hh9TD_w%~}>|K@Hf2OD~}=GYG4%|CMBiV`H{FVo;L z1%B0#MIVNp@S!cq0HInk<1DWJ%2`t&zOwVcP5H=V)_u5hf7gmZkt{~u!3FU02VHLA zbp+bZOuxQl55%s-JlhG9+*q>A8I0X}&|%-x{XdZy zF}QyxF**eq(Ae#jm?;EYgA!yI~ zldix%sElY)K3HhUOsH~Mqfu2`RU3_2rQ0O04@pR|^44HVDlxXGXWrZLNq;-EENsHH z!`G~?GP_R36M>{46|YUG!#B9NTXGJ4op`E!>|3%(fT<|5 zr0Yh?LZ+vD>CE46v?@D4EUj4|dd`wR)R8%GCX5ox)(N<_-9K~5patv&Dpo#3n^2bZ zZI2{_SZW>O4ppV?k5xsIbh}C>of6b_=>#*EDEE`OYr{?Q=Fleh?7o7?q73l{E$rM@ zUzdZ(5=_NZLhyJx73O^vvjX@eO^F(!u`etb8h8QU)_KB8F11UPjD$)9=XU&yntd4~j5TuZ|C?F=Z|%>CqPpx` z&j|5ig0!^qrs9GYG;M{ZUtDy*Dz@o*o09YL?M$)M;5bQ3Zz*}!=GVcf5)pxi<_gK% zWCFAPcDkrF?k_ABx&rk#a$lxqfj|+LDn}$hy2VpUfqLGUja=2ER-bQn_fCI6U9lQ& zs%Tbs0+#+*4Q?fF=QtR+VFxGFPgb6_dkRcwX%{HV0tAM^+1DcLr}B(kc?jhE%Yc!Z zChs6Bq?E!?Vm}u%8{qt2Su%v}y_!WiNzoW;Ub$^(ZODuZ!=K!XUi-pQ#SlAn{<)2X zJYdO3i>gp-RGZ`QmkB%@L775BA`L#qe8*RgP4gTvjSGf{7{RkigKJ3y&74o%WVM@W zgnAEcI}R2WrgZU4-7b%x?C7@=R?#%gjNi@^OQvfsmKG#$!wwQ0;D41-kC#mFht3+X7?a62rW_Ft6TL-9SEex^Fpg&Q^1S~#v|7zoZkK8Kqr{l?o8(x6how*J$@-;Zq z4gO({77S>g6~o*MG8X55yrXU#HpywChy>|WqAkoDbCyECn@?FtKYcz>n)l2*u5Mtp z!R2AtD@6&;z=S~g%(Xow!N*HO7y8^KGM_O_+7smKC8!Q|<2rdY^Ad~^J!i! z>bQ7X#QVoJ9-PHLHm&K;-kcw=$g-f)r61}?mFus) zNP(|CEy@=X_NgM^9wurK^tC|5r-R6Z`q3PYEd57M%^<^m6>=|@YlA9lwybI`%;V_4 zl)3W(F=g!rd|Cw;UG~nBJU!$%VLStX*oFK<$0{L5nNn5&y`&>KYw&10bz{RwV8kUe zu4|q5f#^$6L063+MAt9k-pCPP8w2d`76}2@Ln_qFbz$ko4Y{X`+163g{XY;tg0w)e z7%Y>^IIsvtQI|tbS9xDXP@sx<)Q^Z#9m91JWzK>NypFvA~jh?n9Eh?qG^NWrW#4&EW6M#oW~Ju@}-N897}s zgz2MAFS;(J6Xg)bUiy}a+ozB=gz@W{l2ktOHlnBC%4<+WsYJB7Y;tcVqX1o0q_RQ~IKffSlPJxN)h&^PsS zI$4V}ZEol51@TWUjYvG{w0|92RLuyZJgR$_Mh`2!WUY1W9;-~UwJeGn*UI5vKoQBJ z4JbXULRlbXV~In=u6=qDjmA*O_}YQ#T=pE$s(-xIw=84-_Ywc2UtQRK6-Z%mmHnPL;Xmj*b0}PVb zVK=+&&2Hj%@9xokk?@!HIszrdvWSCcIzGulE?>6U_jMrbgLyW-6WMW+JPBpjQsazW zD?Dy_>jF0#2J?oq!|2qsfhFt8uzE%ge=$`hwg?Q-U1F;2=PN36zY9P(X{TYp9`oi- zEckJz1sh-=&?+w+Gs469{)I`1+Y(yRzWNrstV&wf1J{K#!(} zn6HC3|J7b zwOYB7But}gNxcM-rTFuCn(P<0KiIM;wA1-_1R-$?)<}9M0hMSGkX019C600#v*beIk z%u^s8roa@oQ4h*6FHFbA3Y#B7Umdd&`$pn!GWA5#0_|vu0D)Y8Y*s$P1tRDctn)cL zmNn?SCBr*YTz5y;1*_CmTUA#!wOzQboG^ARV9dj#=V#c!x#7>S0Wnm)8$rwjKKQt7 z-}9wLH_R?BeM$X|f#tN! z#pGyqb;!KRHiy8JJjJEpulzydFaE$q@K64r@rpmN@K_tRMjNt^q(#iW7bvij_(7Fs%j345<`0LTk-5B2EB)LgbZT-)jwbdje|uBmH;nvJc4w!xD~V^;f^6N30xxUC*1- zPj-*+9_~zVbUJ12m+X~Wz&$=Dtb`dHA7PNrKpK8?3lkx4EWaBgKE5&~YDFf$mzCg2#m8`N87?3zcU_*k@dM zt+qsAhE?H((&8DaC=6CWat{)f{>n{^KsmK(@J3L@wVPNO40CJX(2efOUg=hZX@L6; z$UmBj_BpKyNoUbRzQ3ABI4T76h&B89a62ssw6*WdpN`#LEs{Wc`|xe?K-XL8S2x#( zgZ696i1-roM>?-t^h4P{fdyCw_I-`l4mmti9q0IJ+BXt(sRl(3QBI-Be&qQAy=~w(LbMl!un`OOizW8apssB<`!T4*_iB+yDOPfJH_l} zh#hxTDk_9`p(@7LpxkOSEQ~EAuvn<(Z^VFk5kL&+{hb(SPe`i^`x`Mpe*20Tczu}? zDBW#Gyn6ACNFX{{Pxe-qHSGL_EgIW82L$~#xBWKn_D9==z?u3yXJBolkp<#HVjY|; zrAl@3lcwr)=E{LysOLQ?s8MC-j+)ydhJ#?}k}btvnZ{maKadrK*Y%u|Ci+v(rg=k* z1PbaSW7_1{>l{PdkYzb7Do z$U3^3kqPBjSqLuXKxiZW@A(F>YmlL1;t}9>`1w~r0_4$kI>EFfFe|_ND`BC-{u*l7 zg95{PmfwAGvu*>Y4fvzUfM)TlUttR%vTLfMZVQ0c(SAwA!q_CQI$$ORjD-LCE+AIuU(kb&3G}Nu@ZSzL>%F3S00AKNajinh z)dwQ5{^eW#=g|s(9%&T-YwEH9LJ1E=9f<$iFiBfyZO#W@h_^PF>PGsX1JdySZL&hg zkIz?B7= zp?hDsM*N3z>u}#=;|&*>_r|>D`tG_5MZ-jLC`yGf!05TzZ(ewxAB#ukY?YT5~^WRJiE?RAI&AQTpXMX5@ zNLb;6Uo7Ze#n=d@TubrIerl^fL%8?(v-6kV{ zqFo58F>gacpx7g^6JD5UrA7Z zF=q_Np3@|6_>6)el0YHAv!M+J$!Pbin9u&jJId9<8T;c^YD2Nr+g3T-y@C?X;H|Rx zNqJy5<_4(=E1HqAV^Slk?{#Kry0n4as&u9?Wp37S*--|yREgp&mPS){hx_gux7Z%rL6e? zzhMJKNN8tC+PJZ>W8I}IpvHI$u^xHJ0F0{?(^6Pk)%<1DHAvHsI=-WANIV8@g@y7< zwH}dit1pi0LXDiZW8kc<)tYMRwCQ&mJ%;`DWN{x$LzKC77 zyF#CV>nxymTQ2u!)Hk|`i z2?2?c{Rm$Kc71XG`+loV52dsjV`HH#98i~n(!|HWN#&`-eZ2Yw*^V#2$uPqdl~jTc z7OQnMdM&2|ef&Ao1xzkgYm|a4=3R##3xY3$TD4yl1>b$fCV8{rG|HM%Mx8%UO%OHp z(M!(E8(!O3jzWQIMGl6@iV$hS}VkGz{ZA!TjS50Gl+ii4$PRgp+C@VJ2n^#L!Pm5T_TQ=-{Sfy zX47S4IuZ?IhJ0IY)d|uuA_XtrC)}>C(O_Y5Ul+E=1mrb-6)sOct}5yjrh zjnQQ2SA3!~zrosEN;}GslI+|@3#V<3k|=iM8@t6Hk$juVqx5hBlFXLzup8NGYm@Ar z*-M}aQPrvubuY3X%?+VH{Q=c zc8E2MqgcuTRrRg$#4xsoaZ^xs>D^kq^jdA4Adk9&8!DlDSQ_en{h1j8T7d-34uoUi z)`~gKI`=Bfkx&*m18f~Sbfjv+13d+Fty5LBr5|elkHQa=^wDYB7U?25H^Hv9*w`|o zhuX?i$ajf#wgvH>eSgue>0CxK)B628 zUop71Q4)%CmMTxdHyf9w`g9N<*(E&}jR+cW(z1lJC@ffP(DDkUa$8Cb`*#USE;qt>OTK<>=r{n1 zUH)73mC_PfAGq}#pj}_EHB8jAl16KT+??)Lb{FN*YA{VF!&jj%yBp_ElBT|It=VVF z`R#LL@qPOe(0|`ds@6oR&K>KsqX)+3i#`CFP5p4a-3Xinh?A#w+>I<3r#}{1>Qdid zt0PGeSL(?Akmpg~AAbTR0RhGcG%o#}fS@WH>xXK6e7g@)&Nwa1$V^Z($V2g(8@@eR2YOGf<)*NXH9egPBmO=*>{~8r@3~9ar&}H7 zcDyq`-+sTO28t;WDrxO{hRLd37lb^KRer2@NpGcF$LLA%O0AH|in@JEE^Fv802lmRIk)9ag zR_+APA2d|tbnS-HQ)c3_kUX0G5mQ^mbxBxR81m+=4%34deFYv~?ZwY$Dkfw69x5}B zz2|QpdAGl9>3%xLY9F0ZZD}3tdsoI88C-?mOrii;OpXnP)^pPr!oDqmak}5z2xKwB z#QJ?o>@Jr_)YDkh(&PH(X5Er>&k`QpnktF_Ekg+CnVMaX(JE9lp9zePiPnmB9S}?# zCMK;)6ceWiH)Vb`15cOHjs@7wvu%M`%-qQ*3UE!MskV|>87B2)Oy{4~7q0E&{jtM& zv;0J7f1RC*4>u>FbQm43T}^^J-!Cw)?)MvO4mT%l&dp z=>>8@aJPA;@$2{5yVkk5c5B-X+-L!%uXM9|%DsCx!GDhMx6FkbG?&WpIKJMhF2&}<=)XUl$iQitPfZ6O0zd@q`XONvA@C)B#+x4f|3 zW(w9f8oB%yLI(Gwcczt!#wtx*byjp*v|ttYnA8yZ`b#Yc`AUi0=I>tT!D=(QVrp5@ zpYJ|Jzr2S$S+ge8(B;OG#SY$2bqe-=v-pBgaK+kD8g^IC&>?7+hfdJv zqd4Bz;vXYzC)PVxNBY%jOwibTOD z)`V7etWv_qh&ZZq0;)Qud=DKg-uwPG?ntv}9!7UiXiucAk<4CoZlN5K^=SX@J^sbF zuT+yKf*Ttc=f%RbbjUwS({nD2qLm0z9?%+iiS@^|u;L%M+bCz`$-JUHmmzx3LNZ%GZ(u;9V z_dZoc29*2AS^ne`QfO=F$RR#LBk5Onk{g(nIRABuOGztt>Ehow*R|DuPz1 z;SIAbs+-B_J5aUbnlDZ;G{|-jP^B zVojr1*2XW!OTwCC2aC3uzRG#^I{a~YZVPqTW0FD&Tfec1E^U-eJ^7JqpJ`pSp*kn? zy4)Zo{|&Fvz%zHDLrQnbR(CzmnxL+YVy$fH)TJT}T(QKmc3JJ^d!b^`Vp zXl(T{3r&RD%(TxFKJH}e?Y>Lc`0zgVO(I4;H!(D1zHL>9t*?FsIr;jauQ?&vPU3U> zVvm|FMML0&riSe-s3KX87u(H$IN7TSJ7O1R8>F@nd}B zUWxo#R6>mu;0blC-{1N=Pnb@@9K7ko2$tTd0~8WGvg53Wp~JeA`8a8ttEU%qpaRd> z_1_ZRH$T=djihsosd4SZ-%dAUDrqd~IeP9~N$1u_vmL+&iS4dnyRpplub(7>1)p}s z2V&m~_ccf+Qj3NHbEUG-N)X=s=LSi;Yu!b&MS!7w%{PHIW$0sT7n&yI@m+h?-A5_$ zO7Gt{)qQr4G@QNYO>D`7XMWo}8Q1ol=ZAEB-uBORHv= z?i5}8?K_<43m;HOv?0mkpVoZy!Q97sMeu&Xhv`l*9rt4xQ?NCMTZKWxzn&HwENIvH znE~M;`ARp1=0Kw4vEI+MjE}9MtC-EmQ+o|=`UTZH22|jfjo(N{l^^P@+O7cDdrkv# zlQP2>hfLMe1MwCaU`;=rH4Dq(j`HIwliJUg<3i!VCg(~(+&~LYsb@u^u1bY%3M{S_a9e09;?GC8}K~sH=vj? zsN&Z;FKk(cd(97Zw~sEeYYZhMV+0g&(@}rl#|*Y(Ww(`X-QmYYtlyt>&2GAwq)RA< zcUiMLx)8*}IJ(ISO4pI9V@u9vEU{uo=H_9!+7QqMx{Utw8G=M zL1N$5s`1sjjBsn_E}fY3+#4TyZ_z&8J28ElH*#xJE-UjyHbXA7Xc#r!-y_TBj6H9@ z$TC3n?o=|}%1&N<-KX#>OmR=7)A=En|E+1goS#KDFs8b?ex?#aR!okemsnnQvWW)^ zGjNKON1scVge!u+K%4UX ze0Vx?LSaK%N_+4&%Dg^e!LjS0<+26oh1}LUKY|moCiA@KsF;4H%ExZo@SaMOsCU$T z3F9OIQ}yi9v#qjUptXzkzK?+M_!CryUhebOe&@p}vixCpU5O*+9R6K5N^!6D)zi{| zrElC64fCHKQm@R(^jEiZu;+He%)TQBl+NL2DLRO~Gh|c!oWs+#hk2h?msv#Z9!%9G z5sv*|+`Uy;TwAv-y5bOmy9akFR3JD6g1fr}cXyBA?ofD;!YSMeNss`A1uD1&3qdQm z!=1@m|61#wefGWgVLzPjzVSgZYK}3-=%e@EdT;Gl-Z)Lc~zkZAI%N-yg?27gKd z9c3&aiLw!M^~ANKiZ^T?vYV33buS7QwL<2PG!~vIB|eu|Rm+00T}s9Im5~S=JH5Ml z=%klJ+6a}b)*{%V;zj4I^BBQTtc!PY6gP}PZ?U-<6nn;1yYxDls~x<93N&$Ao^W>o zY;0Y5-E4+1*N5q$!TNw(@n{dde9e7LT2S20TTkInxR8D-Sqpzew_ItAej@%-uD0#Iev{|)#Y$fEr>=!pXk`WLS8 zzh|iU4*Ca?0bIN9Pk!a;g;3qw%d~}|s!(*FxqktSzz|F-!9u+DyFIW`KdxD#0&=PR z;NJYk(a2)n)}O0LkzW$?e~b}0i+|_-XY4LT=<1;@km24*+Cm6<(L@-%i57p( zT25|KTKdYO2)$)fHvCDhunkbLp`2Wzkkz>qX8em{?IgSNf2)tB21SP;|AkY4{&!+i z!ST$hrTyi$5)BiP`BwlD8{tz}8xfH{9qWM_&I|b!@&5<ga3d-3T z6Vlb=dK-=GG&NXk*c4(heWzO5R3yEDe(CpX;znVWnyX1JsLm6@ zsw&T=Vq7hO##06AeMTA=j;cVeEf2+aZ-sNCR}MiPrXA^F3Jj?JZJh!)%-t#NdC|ph z7n2lcwzQo;Dgk;{l~>Zx@_4dM8d{)P5iijoi%PJDU0{&5Ymyek?`sO#F@@(9ilH@z z(?x<6{ms&T2>1&xGOc;BjOYYx5JO;-**k>G0?y4nK^?Y~J4DL@F7-n}eE+>1RagZZ zfXe!*79QGA`i|E*|Ii`t@cR;FOy#~3MVIme;XcgH$s{}X|Ju0NeDdL|-u~*L$dz9k zB`?4;aYp~uT!5y4f~O&-q+fBu5MaXoiKnT8bzh51vAXJ5H{v&rVeB1M7kzt}DyIJf$s`#KYP+aNZ2WudEh?fXUg4%Un#_wkYb?w zDfxg-KfCBmW;wz1!&FxxqQ2zJmm`fs*0^~MYMH2I6QLEGwdt(oDS9jZIPJPAddi4} zXmH|jZO!AxljiKruYNQ*_IP+0KX1~fr<6OtL7R!ULm-%V6#3D`-F$G-16Aqx8ORL^~;fpU=lGgM($M&@XdIfTXy`W1R(g!y!h zX=?Uq;UD2}UqpTK_XtEC4(nw{p$7kL6Gp2egKwzb(+Mr<80&#ESeZ?>Z`Rwyg)`33 z(8aeLb{D6|(2@4g@$!+y2!mNsmkw%0DuL&bw(e}HyIPk|N5RYoxvPYLTZio#0?y(^E;ZZQ+6PgeXJ zpZQ-DMZ~<{oCBngzxem&e(~?w95~$F{9c*P;mu-xy#6|Ls#S99WY4S$s5SDzcmc@0 zmR58bOB!_UjJgDS^dDj3mlXG8Dfpp9^!dUyY~VrL_1uU~+Xqt#jGDFvF8$6O zpG_{DSnBAEX`8i&ioVUU-Bf7Ixinz4(t8)#TVOpk{$X^l!Q#tAq|n=DqT0cx4IitH zbaX&HaI|G_NqdM>Hyx=~%hI*>s za>>Wt0M*mxmsd8`k68@xzBl3g85sYK_E< z>OE;EDfM!IQ9f_~N{1;Y$Ek$LYib`5dhzZl33ylH-~51#qMqX41wSPNL5EtsJ>MI3 z?Y))7Gqd^D7wy1GZCbXsWZ&-7|X#%Mt&YT_RS=UJQ2 zau|_L7O1hWBDWcBQHX8P><$&U|HIO|*{`WSN)y9Yx_O9D9ub>5&8v?&4d&x|At&ys zRa8WsaV9n$UaiPswwmLQOwG>Qccehs-Jebc77i5OS>THhT?#Z;Ogq4XA4Juxc4-=% zS<|s{Zzz3FyEp}fUsI+}cXs+O_vA3`fOK|D%|%B?0QzxVI%~MfrTAl54$g2d@K-BH zrA`=jZ^Qgdjxg?|_M(goyVw%0OF-Ol!I#x5g;Xp06YtXx90fSX+b3pcvt~jq)##F% zhu~EjDe%pfZp|`IlzK-t|B| zyzL^%M)}=1n36E8wTd-*;fE^nz_lk-b0#Sl^y_T`A!M}H6JehSZE^eClfEUBosoM& zX9o;{XPZ|3JhfyGWM}0JvB(}#f6LU)7e77iTs|DlSvjY^eM^0F^C0vg=tqA|%}@UA zy_F)JlY>ReH6IP4-4p}(7X)cS1EHodcvst<`|gT8L@RADfN!$AGJM4;#&T)ao8C7? ze>ByR=|pb-D{EJdO&~s=TI#o>rW5x};qk=y#>d3Q9iK(1QQ`?^*}Fe6T#Q8eVqLX$ zSXiJu#QVLcv^%Fkny_7~+jG1ifJqcSDk?7B2;}R1MXMs8-8Q$NlV$`kc=(qY-D-G~8S18tooR z6qO?ft=>k+mUIw(;NZ?>@xC|(tZ`^-9n(*U=*jg+AXe5C*B2qWn!ZZ{YAXN8&h`tY zj`MERK6SyIN}@eGYD8x-!AFeH=yH(ne7r{_sfn$##H@BHo#7`d$8?A zuRl2V7laIO|0?Yy>usfb54m|dVX6_~9ooLGnJ8cu4xAK;SO@OwjDCN$agA6urBW)p z-d>&==7__jrhh9x@Ba77&jWUeqT-8M?--Elb2*WS?{VIN_4qSbE&yWJyAo$^aT4TQ zChS0K?^;>%B|`2bNK_!)vTtYvJpxi~PthBt-5LIKkI&vX*df_htPmA>6o?z<);`~)x33AfL0}G~mD2*z zoSf2%yoY(igd^DNHm<$+@%HNI`Ci91!nhS| zC=%W$+V=TR^n}z&0HGcI?Q|uhlW(pZQI}lTz#G(6o;Fou|JLsE^XhGg_N{kFn&=Jf z!OIV9Mu%d@tpPsGHl`53yrQvLNUhzu#K4YQ7pRQj84;o}m;Fv?yfN-$SUjufTlF0y z;ud`Q{AN?t5_7=)jgJ&(+CHN8x;x0|a~Z3)d~=3ZSSKbQYD8nv1<=jgT^E)ZzPATD zs|Tq^8}MEhM#3b$YuaV*iKeXosw@4;f#mXGZW@T_Qww-pOAvBK5yd*rpc_%gIsThw zfMw#A%&t4dgP(cKrc`OyQH^QEw}z|NE*tcY$MS-%o$D8K(O=}8l@dAV@kY6?T1T`!;!Vs)O5k z*_S47Z%}&sOhcwbI8-#e`PR9Ip%Oh}Mkhegpe|^>mkg#Qp7v}&2pO7vaC&|({?Qnz-FeVI29@C=lQ{#2se;74hbt3jO>%^%_o0_DXa53ne%Z&)Q z=8ffP0rG9cYR?Mm`*j=FTU7OlJw`uJUQU~`(5x5;N_-`@y+KoPKT0n@H)XYWp#pi4 zVYRb=^X71He4(psc{}z|mX{SPY8uwkwn!W8+vdcH+}gONs9donc1j?E#21qb)oYFZn&lSsAG38O={omWxf zuCNNR491v6utVCib}pq@9h4ST=(D7bd!x+v! z?@z{U)TqII#{t18?9bCA_)i%z=xJU~Pu&-P!$)uOoaW^-xb8iAuSCH^#-U-%IxvJZ zQ8s`+v9k+c=YG)vgAl-dDH5i@@K@m9q|^Sq-)q;7 zww{4$1g)GPwQ4gqlP+))n0I-g>U9sdsT%wTi?pmol-)e2I`UxlrGO% zrp!`rUWYu>*=h`+LFmQa6};sPJSPKA6S@-TDHY~X%BCjYp_j7sBHFp(mfPxx_Wjyg z^6K3V=5iQTo^F=kK&#Ew>ZW0vR!-Q?+qnI9neOGlgrEnY<##ywqFEHL9N!3y`QqeFC3Yyt$5 z!*FXI+$U;U&@2QRpCxY=UP;yS`>N{yu;Qxm!Tj-KhqtI>`ZV+77!dB<_Cs)EdD9-( z7``o+ujH^Mj#iK_Tzdg>__U6Eb#~d#B?RYv*H;X8@s`aQEn6M~(l0os3Vh~zO6>;j zYo`cPv$(yS?oS-B*EYtyTZqW`g*I$s-+`m2BJmiv^V4e$@yT){TcZ*}dXPpU5uNDZ zE`EWHO`OXlN}yX>aHp0oV}pQlm^twgf6=7ZnzEtGlzp(Q>DLchje}U#V%~&xM}xbg zmpCuwwbm^ydd|jr^5TnKE8rLSIU*tvy{##60T#2Gji*><&ihY^GQManM-=#+YSYGo zs#~BR`BTnrsJ=!D^&`l;H#1Hq zBr`*3lX%=c`qg+Btsv*m1^*5(ONmT%a~3P;0}a^b0_DCKj{n*298Ubg`iM$G$YeeR zDhX@K4>|inu7`%!eMQfc<*K zZ|qY*GdAXh7xa=bImK@wyK{%E%|kmlKqx}&Jy!YZ$0oVH5jx~#*X64eYzwl2@>gZi zE6oCB7qO)f_Y&pjf0kMb2oUQhT-sM~eswonydfQ8AP6!3hh)A;0`x@H` zeb0F)6{}>^{XkyJ^MO(OL4li|NAE*}B=wp&_5;!sV7J=Zr&^RlEXl(wPVSvu6z96E z$E3NeC@cABHChpyT-6nOUz|HeD_`L%2Q>#zJowaGx-yiuL2hWkLENCeokWZLoKLr} zt-aD>6zn?uV(%iRggnkg<}emc&*p?V0zle$KHrjU>^vCr6s)vOu8)HrRKVY&NiQTc z59(jB5%WCveVxxC>Ts=cO8z1DJ0&xv8!p1#9_Sw6lGe+8LT{ zh#JP@uJ)yr(YL`e#!U3JM%L6r%yZ(&HXQqUpbDe9mU7YVr6AwXB}H}*5G|ATk4Dsu z+Btm+E7BF7B7iRgWstOk-LHTrR0!&2?emRlr-rd2XCE;dp%fmKnBg%*+r!MsixX@t zOu&ZO*@G%jGoS?q=#XiCDg-usx4asJL)pmG?6viDQK$WGx7~}C-NU67<+$6zHtNz{ z3CnekE_mlPan8*d@%+_mUL(EpyOZ|b*y>9{y;EM)7lBr@re_FVCpM(at&11@Qn1Fh zSc7tT>~YABJ>vF~{LVP7_CC+{8(^Qx_=N-DW>*T|nqPgOz=&Q}c|_Ea|L&DhrGNl~ zs^IR{SEch)AH~+XTH=+3+nYN<7b`DSI@3~a?=s%*Bo`JN?@1SY;*v5=N|}qGJY7eO z2KDa7oqS8tG$YY(A`SzIs;zBZ9)b!Kh^OA2QVRxc#3hk@si?}*tfM^b;W{u3*tM$F zh~5ezvdRe;_;!rbL(W1gwQd)z?k=ABy%G<0+=b$~{APEhc@;XvV^sZwI*@5_FZaxL zi*9Gm53R3Cq-*sNvGe}zVX6(A2O|k&gjMH#o$}Sxg7pNYrvE}6x%+OagwKPIJhc_U zQ$kl?R+ZAU{c{p*j-A%;66OV6SRN!Y>-&n@C-$hRr4IU-#x(W8=F=sj-q}HV&h`v=W!^_|LCa$iJj^PB z-|~^A*|R^M3vA%o9y=90`-I~NN!_=R&Y914NZauaNkIk{d>0b3+PV4OYWvRA*Ss{q~qaZ3@sIb3Zwf*Hm zS#nAQn*6&`<9!2=0&K=&20fJ%=zTF8z+ zT-r*aSiK{+gsL1bdgq5~EP6M$lNHk}X<7$eC0Z;kMb(m;bhd$u4ylRSs+zfkV7qqD z@sjD~uwsMJGM%iJBNwaQ<(G-E=-TrUC)qG^hxT~B?a=b*MbALqY506}yamhQQl}8V zg+Ln}a$g5@G-aQ{C~|f$2a&Suu~apQH{804 z?LZE7=xgh0+s*L!c7f>=UEy@_A##lCz~CrRcq&h!59pQy=sR7+_$&mBoF7N1nLteYw$y43D0TDW(hegp-x=_HSa%GM+m z=v1A#PANb-MCIPSLe$@)Wc;Ap8C>MsA)=5R^jg-*)$t^&v+b_}1R*p_`62$!#o6yr zi=5IOid*4_Cb=^*jfd?%1p#h%{xsMCHz@ItB!uy;Dzr}|-!pE1#njyKB_#Gw*nH(Kf}1OUJ*0iQF@M;Z8Uhb4FzSg^5Ini& z&AB?v^}agh#;ULu$D*bf2K>Srpa&2~T*lxx%mtqM@b`NB**a(d0C4)hk)EChzt_nP zXb1dtq5OKj-@hLM%Dw*ELBtAyeV9A><8oLR)UkbaQkCnpKEkucTQX|wJ@u0u2JVdR zV6Y25%;O+^vHRCB~^#wuh8mPptxue#!ldr zj_7nkyQCw6N!MK^uCsNPS1w4&6#nzdT2B0U>vXviC_M>521E zlp>TnuynkeNm;HF1@0>`64J3B10C4Kq^u9M5;%|j z&BKf%^L!oUvJP%d;SuKNuh@{bWHATyY&~tFpp_&aCr0(MVAAzs9>@vdi;eR$Ihgv- zt3hXagz5s6vJ{h%Loa8;H`xaDjJY5WXB;#**6i3n<~3@MPHKd?DkQ4xo8r%dbK~9I zD0}tiMstfk2bcg(ZRtL5t35fz7niBPLx)VtN2d8p>O!xViNwZAd1sv@swo{M#SKid zC>;-%o{_h!AIQ-WzI`^Rf9**ZQU8uWt1_I~^r(?y3`0aoGMr-#9;s&uzLu)xEUp%$ zCMZc4eTLSgAYDe0!p(AC&JS8xI=B-IsgS<(eWKA=JAXviJ>t%HD8q)N+#io|ZBoPT& z=wv$~SENZbpW)eu1pJ0#kPS&oJ<+>* z6cs%nlz{y-%V#%Pb2pHsS!xq3&W|E(1NMs8Z=>XPKQn$KXZY3G@$zE@%tF%8glW9- zv6k`JOX9Ho)$5eAPLPqx*|7|R8q2rfcvAW-@a|3t24u`V*rIP%o|`*A&oL9-oJq$g zR~;7TC&0L_a4WBam8d}er0uMo?>P9e1=ejh9s?m?ed#gfYYKjP!k782J zj-mHnm43HKnppj~L{-((3B7Q!^dlFUUoqtl`V7SJ;`GbOn-i`*vK`o)TWD-*CTEU? z4On)~sznk?!X>-`q5$^goAQw8YWkf9v0R^;6-J$2t~l@s8#0(5f?0@ zZ`z$2j0_p^D~vy&6a46lt1=?SMDFV^FfSzz6_l>SYJvo_xU$5i^qu!qUIv_cuJabT zhX){h_yw_8iWS7hJgW4)a=UnMFUkVBRd z*BM|-`U8m@sSu_Xix4Sd@8w6V0&vzN^c!foo%VJr^EpG{^Q>PBq!l}L?fDvVQki6V z{FPPCh~%p(gJ{0DL(&;QfUHl_?~ZYh-ee~W68F%j)+1+)PvCX~ z^cu})S;h>{PQJ_0bPl@AJLYV-Zk-HDYll0_yHrmG&9Ef;q z-=w+P-Fy5iXMj@nTK$=5VE)5=AvHcmI5*sV5!Gca#)Z|!`$lw0@2#;T=JK{z<}-se zU0nmt#6~x@U}4?3Ph*b$UUmdQS9y10j}$7VJJ6K(qQ3>jzQ|*fKP8ui=rn(hh38@| z4=iw9UA%q}*en$8=eJc;*pIJ-K4;SxhpoLL%KOMAK}}I+9tR7ewI=3%k$rpWzIcq1 zV7=sUU!;3GKIV1Zk$}e61c{R86P!QLLD_uH?}p*>)72nvJ0gWZ7ThBZ%8F^UETddt z2YUXS9zB2N+O9ggqcLss^<>MX3x?at++4S#Nw=H>r-1Vkl2pTRFlCi%@_8P+ z5aWnys8r0ShQSwp?Ip+0kXYF{qK zX|LAs#xqgVT<4ykj_gz$vR-1l!?y9&gv~$&Wgu(+8F3R%A?D`uh1!KnO%txu&4JpvmzHG3mPjfZ=w7?`aO;t2MgW4vtzM-yI> z-kIB}>&?9|?_wtY)q6CGoB} z)s1GB_RKaY=D%_XpmPKo55#Vkm=&slP&(=jM*NJLyowd-mHfI8JqcPShYuAZBgDR` z>8a?j-;%;JeT!NBeChok&d)z@Bd?Wav`Tg1Iuy@CFG1!PJL>D2hSoz*cJG=uR=66# znZ{iTFv<^vT^PQnEZ1$DUba(JjC@6u%R{N7S4PSx;*y!5X6`63!_6((sAq`Bz)?Nr zy#ZN74SY1blWAL(Cbo`&*hEe4QkrLvaKHh*h0ocD3K_BNzKeq9d9~rIKic(f>8dk? zcXp}1PwLBlr&P*F--sTjU^g}xI~r(c#jVGj?J%}EaZ{xmra!~fH7FVFFMxbV>6j~7 zaRfFWU&fN^I_k6{Nb`TBNwo@cidoTW#MpfC7-q_dd>hPyW7+peE>872F;`!0WND2e z#`&{ZJhkPyIi3+7a6ah1xCZ6f^;9TCqir0Q}+hAYY@ZF?8fN1a_K z+prtW(F?S--YEK?UK=V&1A4O5jaBi1;>Z8V7CYCIFR1F{T6$*~;#~TEC(k+g`1Zvcbdz>;JF#ITfX^XX?fw1Dx30NfkQg*MD17%! zx95DgyhY@a%-E1N*7)s!<0#NT4^7)1e?7?7T@zuX(5Y7P74pSgfYf2z5ZMMgF*_Lc zwWnZ}G)^f=%g`peOS1&0T5Lm8rj9kKWjb3$qZy;ove5HX6VA57wUSv`_n(>sWVc^Z zI?^3cIsaxwwGNN zp{4iPHmLlwEG_U@Ad~pE{BlPYD{;<K^)eTxnr$r`e9X~FJm*0KI#Zn*R371CIes3vU$-5_#cveNI_z%w+}wwK zj&9wXU&(_kYz#kHfz1`A3~jggnyavlE(uLpO?>{P6MFA-B#v3FQc3ytTul_~)bc!_ z*-U=m<&-l96co#969nG;n1Oe@NgphcJHw6yvZBC5hXjBgp>&AM`Jn!}{sCW+zk8J6 z`Ku1MN`J>~yD7wYMm_(ba?X3{cXtC?WfFQqcdlOd-Z)*{emOACKekopHcP*Y{@aW&T(Y^ugL==xSEt*2~2gF5fKr} zv)I0Z7u=I1eNIPLrwV0IW3{^^b5=^(+Uz|ucR;tjUaR9Mph(+8+i#nXta?iADB)+W zr(6QwX_>Kh;OK2VHuQGa4sLJygFNy{7t}o8s~)qdcrD*fEnl#v93*{C8K5KH zKZIWn=FqKFI!sj;dA}o}ZvqvZSNe3U;Gs#}2^Q4WB&n23r{&GqmGdnmAybG;!SKya zQ+HfH0)WF~q{4+WE0|MP>Od&jOIvXq%Jgd`G&(hWJc`A1UnBe<*fU@yfuCqX>427a zu|y^#CPcC|%x3&M^4LlodJo-F1?->}_rB8jdB9(eQTm{}9zWR{Zvoq301;IZU5+k; zVo4Q?ScapqtqWWo`ksV8=6;+ie4&QvIz_pot`vu}s{;~-jCelU9WZ4cK6%85Ux`F*cV*y;Oe9qO zMF7LobX>)j;TE(c9CQ{1M)Bg{iSFN$D7+N;)9x}IB zbq6xY@Tow3Q^v<+Yu29kT=|9t^S$L8P=Xze!3y^LG@VUtBy*APK~VrYS4W|JUG;q- zMl%EVP-d{cvg9FB{gtbOzE~^U54{4=^D>#aW14#h6`imvgg3BXPFs; zf7rVL=ZVGo`qPyp4CxSB8ZdRTvM-A`-RP)tV6(+=(k8%A->t?4Z;yu4GFci zf|+~xi1|HIp`5R%m8iU|{cuie@r~1( z`%i4UXG`L`_Zg7lYWkD59nY^DPrabjv6%r@0^&eN{rlZw1NDZ{la-y37yiw|u^7}( zRyOY?Jj!jzm%bdR*aF*skif_D3fBKs?;UUbltJ&BJN2qlHU0Ik+)*evJ9#Pm1>nbH z++!C?eL*eB<6v#SEtc6%t{9`p49OKXWAl;!o}oZaV@1Tbm`qp8=ImkP9MxBPpALz~ z(lRWs?|goQa}((FWOOg)+y15_g;C;0ovpV9(bGXh_u01L*})3<>d@_6pY7+`_*0Q3 z{kXTZOG`AtK%oCC2f2?1&?uM!RDo^c5F%5E!5e(n*5gXf=)#ft_n6{n0M*`~pDOkl zHDX5A#n+2G%0WEn@+9z4Yy#-Yynl+XA4jV(D+w^_s_-}prz?Q3H0B; zhiv}?X8f*cr>82dbn4lK8#DLcl82xZ{VzGc9y(Z65c1lF{qJ}Bd)!}{N8tOsgXxfU z#D78~zeoEYd8GgOK^pFf0m5QK9?^SA^cTN#nE-(rpeY&k@1MN~+%0{W1OzS>ZJb!8 zZN(F{@3ThmH(0wK<#%vDLT5C||4S?$Xn2t++)vo_45K9MEn;T=s-h8g5i3&9n@l_N zGT9-bh1mi3Qr|Wn!skZI@sB)a1ouPm!n-T;Y@ChB54~ei8SmUtyLa`)!C*PT9+zz9 zQN`mKYKPy;`&$K#HYQD>crEoqb3Gc+)@-3K&iZ}gB)`1?*GKkY!3Zg#b@l0oqSilT zpYyfrhy21we(}Z8(2w6Ycr%o$_fCLiFy+vHF{F{fGCxl&+|v*LK+V#qkm2{v`u))a z09!0hnH}46&Z9?q8Ow=b%u)!Ku=K0kG%}dyRo9H7#4Qqj@3g;0K1;FDH>E<%Mz2q& z>P2q>Q|3vgnc(5b`up3>Iluks5!hD$OgC_6hu4zto?sDJSU0KvI*7kleJD`DVm38C z^!LTtI?=wKZ+`9}o}s@TpOhM*!Ry0plmEB8C}iQ!>xIH~xyFAfq|X4nFgD}#kBnHq z{bv=ZgLv?B1jR^5Uv2aW6CFeOA1yws~-3n3Qoz(CSOBoszqqPuVrMFx+nP^qovCf8E#XhjFJU(+b zBqghh26D;2-;Yc)SNtZ?8q;&u>=%Zwp-{|MArsK9Pu*0-P>_H(T&{P5G`D`y{^)03}e zvI(5_#gw9vhu5RSVg`j8e@!m`%|EA@%qKCC2i8cuOBvHIKW?uKl763TLo?lYpT=uo zQ${IuIiQz0W5^S7lKO-2Y|om_$Ohp&N)q2};hT@4?a1EU{T@qUZ~GGp+V?Zaxb01! zK1Ko*6NDJO*)qMN2RNxe9*GDl?e2aKGE6XSySwC=j^c4JPIc*??e%8tx@En8GAZbp z#ovq*SA6Mwe9ssFJ*V2M|4_ELntJ5C>LAE?dit$t%3^xGnJ4u zXTxf@17-6wu9BYp+(Am6EGTB~J4BV9LJo;m?1%qMl~(cNgMRx`hLSqKFu79U)psSS zzLjD@S`?L7Z%M(=g5U+jipQG$FG$X#i2#qVoxztN4vW{UO=Lk{TvG#bGny?qt)IKK zn3Jv_c{tUxk4^MCpbE;aF26r8fh)w13=Nsd*xh_^_9GuHsPoY(Bx9GQ_w1-~IH=fo{gG?t?R(&q3uFgGin^d{>umZPOwP5hk!* zmLHU;p08qpMpZxUr}kjZU%##GTO`~5wJ9Y4OxAvC z-=Tq-oz*LA=R|B;Z<^x#OJ9uLJfG&pw;K@$yrbqqZ8cP&7G^|rX_aob9KG+Eno zhwa8yE8Lp_moP!2l;?d#^RSQRO^j_>-`f{?hA&M;8J8;hK7UX<(zJLw@$K5-`I_HU zcP8<2Dc{mS&bK(aKW%aROMry0r|n_=77-PO#L5*T4KkZ~OkpF2@vk`2_5N+@JO2PK z#=a=kBFIfeUzx(3+Cj}_584e~YywJSdq2fyL#8f4ft=&zcYl)C6ipIPX%M_|?a2{r z^JDuVU8F|2Nl?evxbAk{8|m-r;4eJ$?>3_fCpDg(|vLXrXh$8sq0i{vp-F;D-2cbjaN`NMAbErPHq{*w68wk&9YK5@n08j6?XK_G+s3)k128X#V zbJeB9yV<5VDQj;Ow=~I)4VG_8!P`j>E21sK5@tA&tcz;9!iuxP-~cP!ldry7bDps8 z0ZV>7C;%H07ngQ1;~U)mB*yW8!t3fcb zXocdmqd6!XLVv+RM$G-*w7N;lQK6hYc%_Bobi?dMNqO)qj28zD+ z=={@x2p-K#d(g<9eRtTLM1}}S5AqRh?5N&Y0c8a2&yvhPS3Qyh;jR`~K-=38azhNn z*HP!|iX~2)Qkm_&);^|AJ~?W$U`Hk}A8jAKzM zOX4@D4Q%(K)2#=J&CdQf0IsmSkvaHNw7Av9TUX=&b1RC24SN;lwLBO#_;Yl0-TLCO zPz!hX=;@?cUcVswU*}1J9O(sJB}CLqLv!1A;eY5B>Ym3YZ}v_b3Atfw{Oz-1rz(dN>8&A3i=nkO+q`KEB*Bmp!QN2BO(;Y#f{fkEB=Pr>>hDa7(@M)FGl_3 z*#=}<7``@cO#`u~A{wS1^AC6b}l zWHz?NN>Io|8VVVlcBRk>I^&D&Y@*ubuM-W@q3)7zo62p`x*Zt8L+xVEjY2|pHID$q z{MTb!h8BLeY^(PBAg-()X_DTeaPf_19`2>Qyzoh(`&k|OWY|wU)Uj37L=d>H$M3TH z#Ld#gk5KscECm7qN9hURh`keD_usO5pf64%;KlI~!Ev0P8&7|e6L!L}f-+QO#tkB; z#K*Mv>HRVDC2S0HxTGJ`SitnyztWUVH`EI>|1k#RQBD1{Z>yi@QqKZBi3H@=;fD1> zRDDiwA)7xX1>X8agJ2#dT_z#}6epAf@HY#5yjZ2DIPOd34aMGD^zZ`5r2Cg5&Zhs^ z93|!7!k=1z6IXYC^=6krx0uBMFSLP&o!uu0S?*aj z5M+cMB_VCq$+8QmgY&Rf_t+Egp60hVkU=7zrY;{19y)(8r9mKLN@~7G-lXEGz)Z^%BPd-255)31Ay^%X$_l{FeBtn7O|s9w+w5M# zB=>j1QNbi|`~D1njKob3Yu)k1<6;Gve|v8^$Ap@8ilevv)12afSPn?6(zPsVF=lXO zHcRAG#IvKQg;AO;fuIXj-Ts(b1iY|UU69{Q@F0XNa!X}tEbJsyt-F|-C4|G;&HV!{ zXu`_49@xyp5xPD)4ORGpdRBX;{!~EYmWg~i_NAvw-HgtAAH*;dF$a0?(j2)8-+N5A z!gRY0JE=2sf%>Z~(P?sbD~@rUB%4WUSw4%4lf?(2m)){+scbHD{4r8DJ&*r<^0*ix zvOR1jt=Wy~#LXXHb3bF|Mw~2zUo?`Sf{dn(LSo-40Qiq?8dr;lwaZaH^yBGf5=YA) zn!5qkl^ib!c7fm{7CQ@XA;ih4iU8b7(YL$k(U%(kt~F%1+1d1O`}9KtJC;2=%wS>X zk-qqg6Mx#3mZgxfccv8HPJaY-G~Df+C+nI*y)bF+zci%O4|q-`Y7o)saN5rQ3!NQ* z4){2&OELpp2UGCZ|vfBN`@N-#Lrs8Ua{- zqqYkyCBwGZOr*B+O+mZPOmbM2A(B>jIezoY!SnQyjEReA^I3$$2(7fxRG`uodaLXaTVR!6{{lU#vzF%VfBf0nGs#~*A07jDpB;aCS z(Azm-Jo!*=Ov&zm zP{3`(xxQ|dbFbRf7#C$k6|7>Gzy>7pJKES0?#!Fo=wC&`76bM8j8)UNjR`F}>u`H# z&aDk>`oQbyoa|KP;t5r4S$z_X0b1x%o-;-d81Z&f{s|q8viCKH{zA9ivwd~N>R5x{ z%+0CfZ4ZqQ06D(T`uW=5;02Z5MJ(Oh?-Gy8eEey09xq=XpFG{8s$M->r1JOBjNB&a zn9~Yohy5&5I5qP%z27N188RT8oh~ByE+~rc#;y152YW^DD4?|sS!crnc-H5qM|cXT zCbNTYcZE6@-zzWaQ;9%CTyer+1|NHF45vFbo{bo^(~&g_rW#oki+e5C&=Su8u~wDr!+cj6Da&ou|r$dZRo zN2qlzV_;F4!-ItVI4(1IUa6kSC0}|$u=jTBupvC`;)o~}v*ji!<_yO#yu1D_AY=`Nv{yfmI>ZZnvUNv#LlDsxR?@&*COArk!jjQ~NH+lXNkD zVObMmgp6KnYW*Ur6&JhCp_8w$jkh0Ff3}$SUT{ezenbCe1-*y@WhD5XF2BulPc#%m zJNDa0+#s;<+gH+@t4XJ)CYQ18eC`+Ey>PxggL%xb6JJ?ovD@+-7Ipol+o7MKo9MZr zP)w_A7effavD+iho!pNt@+i~Ow|2C`i(rC*ivlJ-NTUw5#LdRhY|6!p3n?~=SAe9j zTHFZFNNelCu)vVA%S}!+-2Ua)G>b$=_C@xSsG}pcAiolpDd9h&t*_BLgk3_F#g65R zlP^41=kK35&b?F2Y%)V7C?~&I9kH7G0^P)xXuHvIfUyIOGe>sl^ZXp$D=~sKW?|-{ zN(Mwz0DWo4K~tD!WlryV_FUM+_JZq8VZQB^S<%gWi|<3!z|oFy^Z?tzjmJIe4xIvc z(1YBZ_&sA>N9Bqo<9ANfS<978qUTY9y-tHid|&De>S@dzbXr?TV^eC|fg*8Ik@5n? z^WBG6u^$m${bmz8a&jETP_3ZvB+8KsJHsqiXv(*WiQglZ>@;h} z72ftu?)Z#|!DRkgsX$eFGLstr{$wJjv5z0~Qb1|v*k1jM!l9+#dag`^_8sDJsfYxO{s>UH#gLbAa z#nabqFYAN@L}GK2kY&VX@HC2M^NUZ+dliWNaC1`usZ-AG+kpMMH+DjE_={yg@5aja z0NmSS&WfSKV8!yXz75{opo}I~q?aQ_cT|Q~T;k)j5kY~hT|h{{%@mVth9C~ochG1Xmk7)62HK~xbU z$$#dfW)*Hwr&`r~w0~E-X52e^Qcir21F4P2R`LkxKK&KczFNsPfBy{l1h9F54Mc|x zn+n6Pw#jLCVl9)4e2b-{TAAfuLKbIZTkpmktq~MRKR!N@NQm}`%)*o; z=^|ew?y&k?{j9dLzuw|g1@ns$ObW{KpYy@^hhOTp^Icc3ts8tU3pn*mSzpb(^8UZN zy7qsj_dnjLlM)h=6qZ9SDQdZkI=)ygx#fOaDWYkMBbTu$MFB=9%*YI@K7xh4rOiII@;s z?WaYnWM*f5(TSoUcEUv01I1f>VoIs5im5jPgtO3Rno^aEYKEqFF;sxOdJkmul=fe{ z_92p2wRml@kvW312t|R9h;l zH<47;Zt|s_BkHM!INLL3Oc#mm$A%Ly5VC-icZC=OQ%bH1*?G>O?juM=%{yr$lu)i=|_Pn#%mWQP%4VaYhp*~XEXFc!RGz&U%e`c}0}u#vzbnUc;+(37>U!-xQzb#%9d8LJpS`oi4T*i)z3_V)Ut<>A7zsYCB{{N4}uej}@B@FIt=n zb=yQm;K2CzPwi9q6)n-jg9Z+1f$T)}bb{UI0p$BLMII|ji93JzAVjyK%f2*N);ko0 zKAk9Z3iK_c9hb1k{ z(anx|a`kPK7HL(8^m$=~f`zKtU{3q;i12B~$IJ@y^*x%&HSl@v3Y&EDrKnCrQU8n1 z+@bpFl$p>$iM6^W=G0v&SzNb8rW`h+jE=h>%u15+k81KUcB)m z*}$))i!MVNRZAxs*B*iKJ$8|wBBuJM*F5N>Ws{gPKPkGNiDEm)N-y#QPc4L&Y|oZ$27Soh;;* zSfs#cIOS&prVjy$a(5*fs#YPOvyS2QN>UKi)!CUrCD4b}$Xgznw>lc=dsz{yXGc9X z%q*(yfSBEzbDGBVPN_9h?)NVplEoc~?7QV^(P9Bbn71__aMF`G5re*;5PwdY(L`0X za_hAuqdD5}6mUbZK#kgHlGfW~qU}1VH27ufw(-9)ytlIHKrav>mk001qC0MUFnZ(C z!~DjF>%{agAH}-9x6bUr5&&ta#!9yR&_3+_ISfpB^!t*Ng~*fkg^-T51vWCA<~$3F zGK=7)M>!K+;Q{8kn)FQHmC{{G{$9i~0{Ij}NL)Aww>Q9hB$lA?5Jz)3q3MTAt8_&J zlsNgfG9eL6`v=8e^jO$oViYJdYpn3Ji;L$`w&Pk4aJnTZ$7YRxdcCy8xy6*Z5kf!e zOjAXBG^;Z#TI+z#=kz)BZ1Q45e2_9fpd~?>LFz7b7?vDl&3-@d6Z>SFp$y<$cv$)I z3u2EUF3fmMCyRSY&0h$MF8VohqM#apLDH>HIxxugvSLAIZ`4qBySllvH7MR^FyTBS-)CfV zSEoo$D1vTKYsUlo7l=kvZ+bNPM4Gjf0qpiVRoTeoE8em-4MHX$ZEbr>B_e%Dd z+xy97U%_e9lZK<22# zs90jxLBiDR#?2p}Fjy%_>?Ur8rs_uT$4f9TPu21X(vDl#`R!!s(vC_%xv!Nizu#zj zad}P2NM`Y>;#(WyUyyT(2o(^?u_2f{N^rw4{RV-@FufVw57ARPu|FXMER8L~>oT^x zg~m`o3b+hqUOLD2#$G%4TeZ+8KRZQ>|~u(O^29=h>Tasln+_=+wS z#K&_Bgm{OJORW6FRGkq77<7_F{XBG!%K9CgYQEwAVS|Bj3`6reUDdAE>@}{HoO$NpY*IwEZ!N->6xU z)HhE3Umt9al-&8=i&oHlbD&z(Wy!Xzxb2})F9-*ZzKX8dF+33T>tI`l>}6yq3ZnXO z`i+N;b8kWdp0-y8%rfP&IcQl3Gb~S8Rf-#_<5td9Gr?!%ANIp_`^2||=uE2f2A*YV zKwJ0eO-z3SCW0#!8=TnlsJs0_F);_+Kp|%J?7!m*G_3Evz-HZZKvxiD=CBP@8bj|6 zxMzn~m$X(5%qKzZYGyD4f>SwQ?=`^l zX7o6VaeTE(k8}M&P%clQrO>v&(#>S-a=7hzneqFL1Ho^`ns#@IYHR!~bkc#PqyH8< z?#b=}y51UTvi=g9fdmTORgu5dU8_g!F}vqZSU9TO_L*(^{@KOJKadJ>AeL43G*MqX z?Fm2FsXe4YF>V!f{PH+}Qx~2cQ;9U`@SHd>iZ3iHHgyO-X$3QSbQz!*Nr#L6JZ&Zk zaCHCJ9dyj#rFlG?F@tMu>T}`5>Kx!sNFT8973Y6RO5N#p9FA;DYY#v2Wy9zjZ(qx^ zV+#b$lfaPczJllO*;+-@K+KDY4K}LIY2!6PqVrxY6X_^c!<~Hs1(V0tOw4uOgA|{y z|H%kcEG^JbxqIa`m%2VOD8F$oc%GKHaYRMWkiK{^yEQMeD_RXX=Z8F(y6ZwcUvSDg^a%nroS!~ z+KC3(M7LP6oV~{VJQuWJJFLqTr1i4>-kyx}x;dl$DAkCu-%aYH5AziGc;fLZk}1XI zH{Yn$b;*C1Tg<8rlaTM^D=LaqX|+ofp+(8CcvKOKyp}f0n$dZ7t0Ea;0m%g?>#lof z$j$R!e}388Q`vL(Mu9OP)j3tZu4LegI-YF{Y9^H-Y8%d@gqXZU-SJr!?Leb4iC;ZX zCtfRs%gk`9J9rD8vQ!f-xj{{e4oQF#E8Er=%x{3yt{x)4Fo-pt=TYI88vWUH&eG%1 zDW~C<5+Cgy_zXp7z{+z@7&HdHv$pL^L6whgCz`z4HezUaP(t5s+EzTUNm*E|>+JN4 zSR3J_8&-cfus XXnX(@vtN987w}n{+nSL~uf_fsuR?`6 diff --git a/docs/user-guide/idea/idea-xscript-1.png b/docs/user-guide/idea/idea-xscript-1.png new file mode 100644 index 0000000000000000000000000000000000000000..711103332a827a20494816a7e20a3e8bee49cec3 GIT binary patch literal 52858 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 Kb6Mw<&;$UJ-7E6| literal 0 HcmV?d00001 diff --git a/docs/user-guide/idea/idea-xscript-2.png b/docs/user-guide/idea/idea-xscript-2.png new file mode 100644 index 0000000000000000000000000000000000000000..87b075020d04e2d5321ad2e6c60d07560c2e70e1 GIT binary patch literal 56076 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 literal 0 HcmV?d00001 diff --git a/docs/user-guide/idea/idea-xscript-3.png b/docs/user-guide/idea/idea-xscript-3.png new file mode 100644 index 0000000000000000000000000000000000000000..380cc2a34bb39acd3390f2b8ffbbe0cc4bb18d1a GIT binary patch literal 62585 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 Ml~j@_|7aZaKO-J-YybcN literal 0 HcmV?d00001 diff --git a/docs/user-guide/idea/idea-xscript.png b/docs/user-guide/idea/idea-xscript.png new file mode 100644 index 0000000000000000000000000000000000000000..0a0d290d0c02f126ce6e542df8ce5bfa97416bdd GIT binary patch literal 48505 zcmdSAWn3KFpY2VA5FiAH;MPF!;0}Q_(r9pZcXvy0cMTRicyMnZxNGC??hXx4bI#nk zGxI-lKQk|%SJiYCReRUoU8~l2{dUM#88H+jd?YwHI24dLP#zBMRX7~ni-5N;Vc&3b z?P0+-uN;Iyif`Y(onMt*frBH30|A8;UDFO0T|70Wy*M9_8)m{*mORYlrdCFmD%mY1 z6YJI~d#v}^imiCY1rFobZrROJFYED2CuW@Vdm_aQ)haCqDghQ>V=3k0a_cz}eF2{j zB0Ri(Y=h&);U>uqeU_M>^iEeYJcb*BIuW*s;Vg&KxL?B>0*5B;zia*c0j@0pP5uRJ zOS(sz``pyelS1O~uV=5x z&ev_0e0}$@0pGDByB=BzwwF~GNfee8a%f#2jG21kGrxY1i)7|dmC-_1!}f*o(shsV zs=2mcyEy_A9kEF4vGE|}aqiy7@r9T2$ag1Z)Uzf;=SOmz&N^?RC0OT8(NxLISHvzE0nbyR#o~L ziTz-e^o|KyxLzHAiXE~yv)!5wZR@MW6=a*@zKQsN_uP+qoM{62guW0|o1rE_CRm33O=~-?{gI`*6cLbcCF9WQc?2Q0huD~Grt~KwE9mvLCUL3t`J$WP zUE|uZ&z$`jZ=ac{-7$DM(@b0$TY1CFOXiPCX6$`&>XVzwCz=q>`jBApe2+7xEq-DH zk8Qs@+=v&2+N^?oHrPQ?iAKgx`*rpcyiGdZd4+M47RSR+kLQ2te5Ty{#DLBBu7_19 zr`L5%h(k0wj1~?VgBHZm#nfY=5Q6`V`;AQ^6TC<@Ye%wTB zEf>e_^hM88Y(agxnK}WxJoENY zY40j(-@AFc-(Lc#-@5ymPUF6_xm7DEItRaQB(Bhj(#&x;+szj60DmtHkwC(r5xn99|@PRWdn4DiCeV{}ge>SBCH#My_B`$Qp2fp^VieYgW1 zDn@BKxD(5y1#=>%Dn8G5>lzDf(56$WTK%>hnW<1iZl6 z8>0Cls8_!lLv+m+&XA=g@b+Ch^FVfz1BHSlJ7$3*qc$FC!#pM+L6LWL-RnhDW*u?q z?e$2v742J|#V^vzfNq<`=gr{( z*R`?(UV_qcf^M;Uv>!IVO-BJI!`1Wi3bZVS*!tpu^awfh5Asr zrp#d1(L>4xZQ>5oUu=1wGrNB=XHT76y$|wnvvkY11w)H?AF~fF6>LR{7Uryen7YMd zGYqJ8BY5B0@KfKT%RY}$ejI?C`&M+4LfySIDC!cz!@Ugzj%Obnmqf@R`U*41j!3(i zI_V21twLxEf`aQ?JBl>xm>ob;qX&A3v!HpBzLaB*hl9h%B~tW|NB>R_+O=`0cGbs$ z^tek1#Jxs+hkL@Uvo|Cd1gNMeTEOdb!Fg*{Q)`{Mb9p)@wTpMs#n?NeTomW=@i2@DHS-fqR<|3{^&mEBB z7AzI~W9lFCATKZ`=MnS#_XGXK@U)yhQ$ zvhzlv!Fa+?B#LR264dn=`6zhwX;Y9>;r{BBL>mp1ak1!=*4ZHGKAf%Jf7JoM=)ZQzA&_A|ACUe2@Baa&7kaJ7D;GO)5povN z*czC{LM9fU#pRoYe}dxkk-wp?RMrqYwN{aaMPG*+ESfKGw3gb9erl z38t2ZK2TW(5+{U^;9Z5sRgGIoj15{u<{!{@rjX!X!ILMb*N_kidd#m#ugCdW)HF+v z(=P_;2A(F*_r+SH&O%&?idvwV{rIuDZMB7cgt+E?_!4|lZc>tx#ZIbPd_(Qt1~p+xB7m>ONQCA$kdnSSPdhnKsp`|EO?8>NuNAUaf)? z(IvDcz4v!%{^bIc*D9P(=ViL`7&f+;=*I$%r-kQdA8I`Nreb@jXH@&gad3TFTCSs7 zn`Wrx7+P7M%AbVy9_u874mydpRGy|UBT046chaoNYFpiGM#X@9x+kbay-iWm0`q7- z%Pnq#4+Y^9_v+d_e5M!6-&UXBYLd0xySF4$}j68WPgUn8=YT9 zhEXr^851K}5T&a?+86?#?zQ5^_p2gw+zfe)evqp=^nacG03bIfCe$cOO}+e_XtRwB zd79vhmnu~{~TleHzYAUZA4h@$y1Du_(2NwT`+Y2L>8*nyJoH%inp&r=Q(St>Gr| zx*S`3t(Qy$h$zAOQJ=o(>>;mqdY7`SooD@2f7IhqCiM#_ENmG#IKNig=-8ri}7c_&2fBDyQL$_3B#aSqpPhJ{4=F|Up4!@ZAqTR({{N=u)TJPz#hCwQrf!N%^}{Ja&edqcpa=O7GyT7pa1L zP~6zu>F_*g5vH3C=-qyyGjzK3B(MuH+-C_C3$AnFdSI^DOyM-@1nk~n44pi&ws>nJ z*&Nx1&sMg13F({9Dxs(RyY^AHUYYkW1D>CsH_g!iYHG1Z(+16>a}ihMyD2n=mZ^@O zJM;832lDK116dNq$w?Vxi1i{N95R`yNimE+Jkkv!_3U;9@0N&0@|v#G93l_Ht3TfJ zS{Nw${`@*H;h;dQo}N5v3prNg*&xw*c&+1%&6mHaebR5?>7Df zy`H5LV_C&qruFgtH|4}3C8wIdfpq!sdN?9-tluy9k&@gac+u!|O5}8>ri}Zt1G)QvPRwCi4Fu$}mOx(VGS9*WglM z$;n$mGu`}M>wyR|I?daSs;W239tPWp1er(O3&8P*A;B4CmIs>FY_sM4r+YAZW{XF$ znFR5}qr3Zkmy1k&K@$xp;zYUkM#E68*3*rUfo|jcOE_DPFXHLsqUMM+xd1(&9OkaR z{*64Im!5Qo^rP8OY_* z%xA_3o!^F=b1AcC>fE+hL(2WS~Nn^~<9gXM$27Af^e-C8;UA zR)P{1KU_or^M#cAByL4*b`LXRP4|5AR+`4nILLPb+n8@*y2*#eC$V!J^DrdSl<@(Ju?U1@^?k@j%fy?k!2oOkAbGcoh_+pJ74$eO)8ow~! z@iepYW|aT*o7Rmt1HlIxjOsN(?9qxB>Z#p*WX)3p$NlTv;a&@*o;#%E>O2(nOO(yT zE2sshY=|jD2RY8G_51dlA$DttjslX>v7|L9NU#@%?ikW$d*zd{59HZDzud?nm-gWf zuyuMJK+IRu+UiCywrR!NgJW*~kdu>R%nm61**)1e7JkTX^>kMn9@W_PUF3bbzVw^2 zTNNr|(rop#6OoY*?=)dyBjUBY5Km%_z2uVa^IN7n^s>vWvUJ;BP;4RBH7gQ$m>Ls3 z?`lys_dp5u9%=ZWQ!Qqs)yLI8zxg3|jJpelhqKES^zw zD2bZ%-uq%gi`{S4Acn=`DAvgV2`To{oJ^Nshs$fNEl7Gw*N|U6f3V5sGTW_?C&c9E z*2VX#3Kt(x#Z%^=V~KaO#)-9?g&q(H+)Y0wicW7^lUe`eMk9_4NBRbIwDe1Q4k!adjN8=Y8?2T)4zY4x~lAOp2!L;CBQ-f`W62BWu zhP<7I1@h*JjvtF9kYP=7))_S=!E;mBTCp293f}P^+WP=!9e_p4!h}mj{?=sP9CGB$ zZHn-ELztC8EU5x6|J97wW{KHx76pO@FX1Vr3lo!^Gh}kULvVK>&ZImgos&Xg-eGk| zObLQ=lp88y1b-x1;NGHVkw`tF&C1)#r8I84HxV^F_h1SBF-M#nWoXkRP*yvejT%!n>)%Rzm&q!XYV61EJfqcO$D_ z)TJp#7Tep%5t#=ZfD%=-WvALKSwJkH+-IfYo|WH(78UH14^~mDwi5|y&dDDm!+^KS zFPm%JGuPRBz?#UYb8hiDdN@VKEsD4rYk5z0M-aF(ZM-a(kx5(USeLS>BK_9vDpm{( zCSO!`Ie@MgL~=T!APr8V=6Hag`(xE~#ig;X;J>IVnnXi|oI%>dF+Fwkr211{XwegJtaNQZAK z-<4IrZ8bCCASXAsNA9Cs%YS`mL}EuC6;*TFTVjYxd3y6=Rcvuk4EP0g)6kKHydO6N z;a5jrel_a%dm@gi4pAtxj12SofNK9jUf$Ujr?Z!RKL|KEZWY^Uwfx(4DvM~l6untf zS3aP^{CYU-y$tY;iF`Y&i;>C4VRe4=$-&oIn>Y6)?C#uhlCrJQHzufzX)Oh(@iYf+ zUF`<_S^ubTe!p@+m z3(1K+!!X-VJtlb=1Oc^5Tfj3WwTVFIsoz$$b!o+xhl71;6^bTYj@gg;o|O%3yh0tl z{WV>tvtDxol;+-`t`8+j5*LmEroMi%ksrAyUeQldtp1lO+rDHnPsAJ*DmdlV7@;FA- zkBnK^ez3Bmn~KNUHTO9+HWvfAR8C<0DsQwiM2Yp_*^6iw|6-oZ5$2~%Kz($2=X=%uoit|ocpW3fKNtp= zQjk2Pm69?Mzo67|x~O4SjVlB+CZ5n)uig>Fnp!NJiuRmlSE7XBmMv>mTRxh!&b!qw zNdo}@L&Jfz8iqhnLD7uPggKt|PgA1bc0y%e(cHdVe;hQ`l#Ti`+=s?a#~N$kwj99I z)5})j%%;{E6ODh%j&V=GR3pvPAyTQ2f(-|Dg~5#v<>J835Kkcvp$e`o96pmHQqb#I z40u4Up&?SZ8~NrS9v~&&knORI-^8!(V>X7u$4#u?Zruy^8;o^9k_qNCasaXf53Qu@ z$p}P5bO_;xdr`)y5iws)nvjF$&Qj@u;)5;=eRr4i!{CQ1t^ohg?IWGCV*zM;Mj~+# z=jRGZ&gD1)wWvuXDtA7E?7ZboL>VI&iC_+gOrIM+K>hOeI-4=wFU8V$#Jfrx?_x_& zR+PPvt!3;#*PNckErEgkD)_PmBkK2)>0!h=OQ(KbR^H_KUqAM(zdhai7R5lc_a1qf zLWWH2c~)A>1QwMHlp|)}e-QaOs04Za@Xz>r5r zVX9>=OOziKdqIvAn;NGqnuP25VOpZ^_=m2}E`x^DDsf$sq!KMBDXHGLtw{PL?j ztswKDKrOT~r24qa-tE(vpI?hUL;#sWq-gaN>NyE2hYZ#hZg5^}W_-3?Sr{Nuit^A% z-eC?PDHgvYz&koDiW%rNquzg2Fg z_6D#*2UK+Oqa&$CX*6@_cwxxwMtxEPn7sv0QT{eLJTzf))9?j3aV{0^X4mPMbe3_V%shnqu`dCn!SBKFA(592I35$eJS6-6GS|t7ZrasJcLZ zw0Gr=FqRmmBPQ}uQy1!gZPhep^}g1{@lGG(qU`Yq3?6twnc+(!>F|3I!lcn=7%cMQD%Nc0 zJyr36@jz&o;0U$=Ta1c?WL4)oruzgl*Rf#c@qX7Iz8`X2v7^=p@$2nDHA6!2R07eo zgwD}GQ2}a;2txz7xPB$|2p$fZp=3Tr8X7gn87*sn584_uUXles3k2 zzEG5qL<3&)o)-2WUG(!;2YxiAb+DcEcIrJs;8_5vN>Pn@jes1TQDuE@ykhy{kz>OH zIknx-f*vJilk;rwD>(gEHSiCjZ6=@P;*Oa`*)ZjD+L{d3dVsm~&$`AQ zbTc1nvnqha>_nCS))Cmdc#+c7!Gu9AX@k$B*=UDsVbS&E&suq+^=+l9TlLUEg4r6T zJV2{Q26#eQ9dlud5pxj?GH~cOE)=^C^cb>&h~lEMQh%AIcUH+XOKbJs=P#b=qrwc! ztouc8HK-kB%OCr3^2dz-2TBU!xa8dZI<7@9T~q%}V#C@Sy!-MhVRq+yn6MxUo98AD zz7@dZy<>WOSRh4jK=5z1iQE|w7b!kTOt)b`W6d?R`#rrM;6PD#o{qB`@W#%f@kS8? zJxXn_iN1xZ+xC9g)r!_Eu;)KSXHlD6wvN$xIvUde|MSTDWX!Ioou8@_qOea`vrb z^=c<=xX?51^UaU0E7bRnn+(ej5P;L@n1k~&@3GdYo*ZVT3b(^%cS3*S^tDPVH#)q> zi06gSQrxwr>cWk&k=Qpj%Il~@o7q6>f#CY`t-97F1tNgWe%7)Q?U7FX?yX36P5Esv zxNam-u!j5(@hoF=?Uza?rQ0b6WFv2j{XES3xg$-VO~3r5t>5Rgqb{~RByVsTG_)$r zB4lI)wm z6IhOzuU!beeWt-%V_x~-{0tEgVHhVcbu>RA1vMWuKQANqjdXfzMnGVaW9SpY_)WjOdm_!hhur$Jt0Do`U*$z z&Ic!s%k?ZkMZk){V>iX01>2CbiV6m_ZhZByE&2;iOq1Fm6DLXbjl&)}^y%oH{>aN_ zy3_T?$1pE`wsJik;`4A!RA7BE4OkT3qeSKNDL2@LdHNIycZB3%MW#pHH|320w;tlh zb{K4-o{2-U_3N8Bk4>J%=-cjl=^t0LE)itQRoO!Qyt{r8)i&MmwzySurDT-23Cbi1qn(VDWjBSBH?V1MPK4wQ71i_3LhPtTSTy8!%-P-vyb@UEe){ZLkWmh^9G|D>u4|r%LD~ zWWOSLC+&7FO)p!WYsu2(Td-ws!fr;};d`>nD>TfGr2ZWP%#I1@hVNoRtLI_3zqojz ztF_fQ`(zv2?103ibitmx%_WzleUhb{tMkfa!Ep=oii$bp$ot7@>O-IDLinThlTy8? z@Dy}JppiCE>oJ7u_cdy{=ctRp9fy|#1wE)cy9X>#eDCvU``c)tL~a;-Uf6S4pgm)E z^(Nv4Tw7^Cw7W;II2xx!0F>%a+|Wk-^qf^SMhx(%v366N%x)@BM8X8ZfJ!cvnp%cC zFSy{A)#c?^6mH07E$UPLM5{?rao-^bzDVIPo*FN8>0xg@w^J%!w60wG9VtD(@HVJx zcwQM@;eL#_R0Ujis>$PBa=2S_J!E~CY0jIO*!|+KY1%^cNkS0~`H8Du;2B00n50q4 zBi?!~?Q037|1di^9wKc#+_L^+lEl#FBu3Gfhj$;NaZ!XG$d%5XU>$E zTk)R&l|yc{8LLEHnd&EMZqY4Hog$tk_D6rI82Jjr+5WXbENqA5xk!@5*76~AIe@N| zt_uIWgIu^}VNhY!REc^8fRj>4h`1oHY5kxKy3IqOQJGrZDy0G59V>;eP;Y1puJzeJ zuEj)$A3nbA8gPo)=(ZrHBXjA?iAw4;kEc!0B+nH%nV1Cvvu`^`d|c|k0LYE57AW_( zu&@C85aBkCQMIH@|1ml=CmA)RYe0#}<6iEpp|XxsRNB-5H4 z#qw_BY*L#hZ0vHfk#OCdn!G~SdP1vWJP2>uQo{vr^=&ELR3Oq2N>v8 zSpUoiTdc{Qa87g1N*L3NbI+M2@1UQb`^k3wRw)WVmTQ6#!hze+}*wp9C>h+KtoEFt-I}u+(RIjRFWsX@@XX5CU*{K7Iyv-ce|g z>Q^|Bv<}#3?X?Yvy<+ALt7T}|w!0r_<+IoXN@-p6c4%yK4Ikd_M7Njsr!+leAP|qW zp3jw8lb|EoA=fUqQgwJ9K^K$UY4lcOAr@=!_TiIsAnm%tu_l9vph)mjflvmE157

zaAZApcmBTZSBJ{}Iyktg1fAbOr$;rUg+22DETSu=Q*pY4v|rFF zFhz-R`psaYMf3%gp4Vjpb<}K{kcfo$>|$!&owm>A`uF`NHxDl;Wvnri_lZSVb*WN< z50~MYc=fKDos0`m8i{}Z<3@N(q#AD77TP)OgK9v7DA@>sJ%D1T94#}U+?mrw=I)Mk zbbbs!StU9P_njXmpC|)GKzz1+$Okw$GIsqvF0it65ECNrVTWJ?n|k+u!(34K*TpFh zK3CboSj*%V?ol$`@aVX>c;4&p>ThOlv-}Pn@u+j+(B;75e4mMAIJ)lJrHVc&A1=9# zwTJYsNYT1(l^wk6QY#Mne~}o?|AsUuYcyMW{n)-~ItEW~Z2}qiag4@UT@tVf7qqsK zCdezc2O~2lL1hmW%jj}P`g_2Zg+&F2lEMyHQ^a6kl_hWMJ%{!GBUb&!S;rgpr5pqdo@hSGnSth;GTTFSv`<+C6z*9ToD9 zI^g>esNp6c35{Y6n4KiUYpMb6MLCEQFb=lV`y{;f`Ad=m*Qjk0q$@>=k@_t9-x&#s z*}pRq8E4vqcoyM?^ncik2 z(8OFkipu3(koPL|SS*rWUZK9u5YVv*^&HXLc@$M2@Mm9ct{~TM931yZ9868eDTqTM zVuaC?FPJ8cZxQikSxl_L^!7+%u-lcA9lN!+#RGrcx^H0zw2H=Ma&Cx=j_c&h6;oz+ z2()`oqAyrE#A)WmVoI|Oi8R_BGrWSUZ-+fx^5W6pd%5*e{Nux=zB9LfXElj7l#fM6 z5m6u}oxY*Q;Mi(M*rRNrS=&-PeKPzioq3|ox!)ip_slRd+q&7l(Zt&$&G0QTqdGP4 zYZfU=hWN4sgaga>?c*AHc8Q=t{})9(6uv`W;AkG6hw2}vxOnSp*Xx01uL_dh)n?r9 z^q#W50-y8*935Z!D>|Qdhx-2``tTr%Xdz1$&H_YHRNmjK5aK=3N^uyJ+W@{yNlgos zlQ;ZZ*C%yCfWp}y-a)>~VTHVYVUzyy+dL}F$TSO)BNkH8BmWzrko}8LWQ!jUhbda1 znS0O($~6mWJV*wd_=y^$`l!C9a=TmmH*gVQoneSy6AJ%k;RX58`Hgrq4OX~l#%Cj> z-5m#mwoSy>2*WK8a+fPJdm>RZp4DLRJQN6gp)lV!JzsQ{JJR3)DWWEh%u`t^M)GLl zbo43jktU=|RQ;N^TkK^voi&#g0(fg6_|tsn>SnX^Pdt8C>5OKtmjDyZWT*ufOKkC{ zS-$#=n0H#|w$N#o@;_QRquVIlu-`z^y{7^R%hw?J6cmvSbJbgKY1_w9slBt}+ z$hPuZ+Q^jD*G|95jqGMFV%9TEArPg6d0kZ@JgE!=^iQO>yt(VyO*J}N2nggQ&5)T~ zSJRX^9=c^JCIjdk8J2x}(5LdT`*RV(Y*;b#`w$DxT2K`f5pNK!-epP#j5 zJvAJ{^^cH(COh44{KKOmAw<29=`#y=X$W;@++~Dd<$vK6EyD@uOFk86lM7bYR%%TA zc7;9V{WNM7WMlT0Vx<9?F*90T`F4+YrPn3;)TvVR-b>k))y&seL?{U)BVLaCej1kr z#RSrfa%CW?{{Az_0hdo)>kEZguj#;ST@8(}z=&iJaI?G%wFw#KOrBh2zvd25<8RnF*~yi(Xy$TH zu5OINS}q)}lx%^ez7rE}xD^a-S@cwv35`XOYBu}Um1}JFklvCXLPfByu@y>Sc+fhG z1er-ef*V7CDWys_&|_f4(afI(%9-zzT!mj^xDu}TPb>eSB0hQs>{mkhzf(2GbY zJEj%~vl*mP&BZL9Wz2x3w)tr(Ys{naN9=uTE18d@M3R+jWq9ztx%d4(KcxK=l&EaU zbuFQ#-zp48@G@Eg!_g^YP7YHbcd4`Dtd{srD{t*FAT>h=pnm@&>cHdP@>YRPRAb&} zuLjDD%Aj5eB?}o@@>t!-;EbykV75Qzx-bP!>JZyJhEz&Pw1%}D&pS>d_$?Z6esnv<+J*U)ZM)s@be9>mZ?h8=_(CeA^l|4qd{jBjh=A8mXegkF zLA_Ca`F8cF77>FMJ#VjmoQyEi*5)N#8$%H6Jfn)tpwoVkvb^8Xi#V^f*KoGQ;%i7M zSP}%oQb#C$3AJ+c2P_M zz4EbSjFoZuvJx2kizR7mB$Xs5`6v)}P0#nFxqmKn?JP*{ub%F=pqoV@NbIu%xI}oU8Ifvn@x$td*3w24ZtOS8L*$k zRMra$jk%Eu%r~HpB;K`;#A2}$-JQZl-U9UWK5R)U^)z+&p!`$8KrksaGxQ;-wTV$o z7?`bhT}+~iW4XjaCo({!ebP>S9<K~Ko z0Ji+dB=MvTY>{Wsd@H31o5-KBVhuNpfRCnICV(b9IuSYdCFYI3s_DVu)R@A<2HT6A zqP;gR@?F`LFR$|yTC~aa_p*D4o66JJ5BVK8bghCYz#B)Kl>KEt+&!X${p$mKu6pN$ zZ5A`2ncj1T(#duDX4&!|O6CQz>?r&8Nn8&erAAWv{f;vWkCkw&H08*Tf0>1o=HCq- zZl`MOMZ{;M;uB=DYD}Fy7WBwEp?+|8ziT10FmA*{?oTpteSgerHbk$%E2v}oU|2_{ z*X4UtR)ylFze9XmQke#E>BMK6x_s9~jMM50IXhVD7t~^bC*MI=z_!s$pLW@6GtnB) zu594xm^B~xDmQApS5#VDc9l0Q%Y5i&F@3*1OwwwbHD~Gq|0gk!00mZUgCTjuC~XQo z5U+7sL+n}<4TJDMntcC;Mv|I)g>vzp$(W@^p!n;m1jC zxn%Be^QCr=WsG4+%~dS$aJ(-gBr)hb?)D{ilc8Z8yaTMH@}qykWG{2!AJrOm6WDRRxFcJVtpEiq{RNz<(bPq8ftVo$vO~5oma=2T99*EojqBVq0kEpACqb7 z1ZSB^h`F30Uki^{uDaD}<)vB1`Z4%k8#a|{E>z<|BMVB(wze+mbeeJhoH&!0%r4oN zRo1Xln{>&iua1V?g`M_ojUT*5p`Z&mK(AxdGS_Z!*DfpCi%rEutO#%ccvuKnIEXoG z8#(KdIWQe;?6=1C{N+-@LNky7rrrk z1`-EJer1Y~mBzO`!fs&{H0s|7Z*H9N4XCi+|9lAw(_u&o^Nup1J!HkBXJcj7{@ISm zmys7L1(isNx1fg5w+TLKa5Z+|N^i4$|k}01T{> z!odFU4b`P-_3aM=iq){hShULjFS}<`@@)6eUJL0y`#o}Yuh3&{+~*uQVt`D~0tca# zLuO(968_Ndl@NLBat|d7XoeWwg)RM1zpLxkoSYm-=WG0`8PCLO*g^AIwy$MjMN?QI zn}m0T6dj9%XccjOJcv&$F)?T-Y_G@`B$N%^kN*+VH!VU`> zJiSSCWx4!h=F#FCmBd!60*_htfX4-!!y?p!%xu z#a;Xj>Q{zRqJ)$6J3vN20L{-a=tf}H%wjC+F0 zyYQo+f>N4Uxlg%CwO-Z;U0>hGL^6~2)XW&G=FE_K62l;qIRDfoxFa zMCmZAa#NKMewXu|CK9CErnlZ_7h-Q#Ic~xxa<+>M;9g`!)zYv zurxd8q*HjGW!5lVZsLa7n|Y0hq!%_{(83DGQ?q{U(pl`oW_jvEMw z!F-L7I>hLuf7%RtC?;?yW5*H3@lep1-Bexs5hwi$RsfY+7l;dm&F3haZN7-IFvnx3 zVwYS$K46!|O2b>W7duhO(?|54egU^q>zgo1;M=6JsmV>#U=0NQ0OFDl`0fxt+-81FW*fsVkqI2UgPi3Yxwx z8x=*vw_;n*&r{&AAY5eGo@m3T#a8L0dz$lP!W`m9t>d~m!NI)tqr6b;OY>op`2#9_ zB*D!uu#G0)kB4Sq`*y&7fUP_ovjz9|e+jQv@B zJ{kFgxW}}Lng)dv{|nT<27*5u_&}&6eS<1n?`yj66l%sS5j4wZ9ikdr|A~{+j_tA= zhcSdQ`)9lDQFa_=h_mE+;b(Pk%egeJhIj-o@1$8X!da99C-_{X0EbZ`0C`f+uB><> zZvl>ciU6k|>!@9|MRpTZN;n0nGF1&zzz0^;ZB19AFMW$ee27u+j-Rogzu$b~hQ;bJ zFIW=S^3*?{ps)mwo}l_CUB}(s?Wsxdp&sJlRQP{GQ1AUzSa@iAXp4*nHnqGILrtET zj@N-YmDEvMjg2SV4YyFwQHlQo#{U(Hy6!c(z<$j6xL`XZux)*Di^VrH>?81td#hG~ zaw$m&$|RxS(sud{Zl^Kk)fMZuztOXac~^J;w@F!X7KHu%k0IvY2mDVLj{SoTDtG}a z8o!u-s%Q}BSwwk0U9kMy!cY9F3v)eS=o8q@r@=g1So-QrgjIn$B|S@$g0xTdqBE+t zye~;h3-HT98GDfC>u-xP&%M_X90uM~L>5RA$MngqEFRqpToKN}m0=>LJSQN5k)W1G#R0U^oSNXvR9WaV@nk*$5#aK4Kc4iDTO zKQ*wTp*u<~ZAx$>hSj)yMw8d$`JLSShh7C5!lA7gU0PqH2n*$?hXk@DJ7PD6m5s6hu_I~VG)=Huv zBOm*H4?(3z=CDag$li=BYAJ7CT3IDC#~H8Xb1H9a_a z!sK5X<=4N1pb9pPLB~*?Av|L0v6`oyF0=Ez_G0lzjW?ez%tXakX#n8VP@WizpGbmh==E=(ok9aImD|70BI)2 z>9baQIN@>QN64cs9)0fyAA#GxGRM6Q7cQIcj|bh_h4e`z2$QxhbL;N@s%Z#{(hbe6 z59iYgm+61$ZPGe4YFXi7w$a>B==`OLiHc4oCpJVNtmCT<{%@V5>kX^-y!bF7w<)u! zOc+#+wy&P}G-f(Hf+~RBp2<-dw3AVaJ8Gz@87bE!N7O$`HrQ!3sg}-V6tQR9j z%><7>oNdfn7;g{tPF`tp-N`9A`SBL&%c*_8A1~BqLT7dwGl*JL8UAra%wzw%wgX+q z{`w5VNJYAZUmPgB@JG9WdgM`7(5jMZP@{rPjs{lYOcGijyK#xi=j@ZeEw$Nx zJu-Ps8^%%gF~((*O%zSRYwPNKh~ZLHVnjM3VkCvZ!tH5dy`T}{9X~&GDVm*iF&ut# zfR!)kI=$LBu{1rCiOsumKKi%5kmvMlpa8RDl&7-V@E)&z1n$0j;2J`hPP4h2v(08E zW6DePi{Qdg&a&ZsUZ<5RMl-ot&Gc2v8lztyO1a(o%*j< z)lDz;Bs(JBpra#(P`V^hReL+<&hec3uUY4G?San0TD;=S$u~PoY4 zUacgx2bFr_u1Wuys z4P7NVB$TFebJHd6y$h0=)Crg6NHV52{h&r;7*(IQD}UY(Ik#6*N?J%04`yhlnAJE} zOXaORr>#OpE?iHSA-S%FT$O4mB$HWKT*E?gBI6uCRn5St`X0lB!w+?Da2weMP2a_I zQ&Yvbk&Bk4CJ3&e?{`C+VORh9a{Oj8 zw|#4Ab>b~c>uJ^Z7fk3e6Vzc^7$lf<7Y!ohZSLqy%DDRcqtI7~-0q<6`V7h5wI+YE@? z;CAM7i;GKZZtla&1Z@pU$wWr+$Jr6`o>fsY$$$JoPAMNRZddTu7a(5J`JQcb2fn2o zqU5wpG6VYABm@UA^lt4C%q=TxQg-rk+F$ZjNlB<`gX~x46u0;&^F@C$nmxRf`x*0# z^mb7OkrW5><3JUn#cQ@vuMS~aS_w8dI6zG`ALqE2*H>{y9T7LP7bXO?Xf?{%1o+PT zp#w-W@Jd$#>IQ^k=4>T|7bDyxPTFbm1pz0jrsi1*J4iwK^16z5SmZOWUW7;m`$NgH znQ8N>hQ~jDB$aw}Q4E#So!O1>U7YpS`&Gaxr6ApvimaSqL3K_Xpwe|ifEpw+J0E`M?y+2APSIhPvg#Sp*(*RfOmrbn)9ZtJ=y9C#VL7Jdeb z{n!CSV1CzwVWekmwOW!6SPpOFr3yQ(!h(1C|EPPb;JB78UAKgm#gfI$%*@Qp%xs~> z%obYA%#0RW%*<3`$+DQ4>DF3%_wL=%eNNoCanHkf%a{*YQFA7Z|NF`Y7&U;%qr;nL2SsisyL%&PjXiy{?U9(A<`!-t=d0X)(VzutVQH57qLLcOhLBo44!_xEl#Vwm zt3)c$V%s$L{uE|uAFGH+R5JOJqotZ~wLKz2z((n$Xd~xC2Lo=7E`wnNHc2~%3K(Vv zsM2na+&WQeW02zz;GE&6v)f9U-q3SJCdM-X_oOEkG*7s&et-qgjqMO$^A(fuP0MM9 zF!3QdspU+inzoitemC~a+tE!en-I&_2*8Vkjp&X<7J;EhhAx*SUgbOuc)n(DTQiIj zoVQe|l$k4<%@joih|zO!+EP)e>UU#61Blu>=Fp&rh8~ikp~Jpkvzd;}vOFwrLH3m+ zbo@r3B-AC(yzaAs1PGCKF1_q6jmcgwpnJYgZAHtW)A@DT8cWv!U0;6G^u|X-_ZKsnJBRkkd>?VR?o=>VAQF_CZNVw;=j0*Cjt-#RQpdfgpH@8#{nqs536N z&mj3~pHW~g?*Hwo7f1OgUw+@fBNf6+0h^&66Y@u=;_KkgEu2K1Kwrf@Nk-r275Q37p*I1Y`Wkyx211y~ill-K z?_=5p8X#+3o8hkuPbgbs?OUaL=82E#Qpy$t3 zDus%ym_Us^r>S>>SmZB*rUpNJ0db&kc>Ij;^$p0%zRc#Xi&kvJhmZy7C5PHyRhaJl9x7 zwpzOC1(@r^1+t?P^RS2?E==qnK#ywX)&_fZyW~y3n^04*_++$X^`j7I2*zQu0;Z!I zO>A~PF$#@`d8;@anK_myv6MjNOPXK`^65N`K4P#T&6P|dvjzoFS}seqlx$WGGa(ew z^ITOHTg)DIjAgQf{(XfS1Y{^L1PV4l2vJtAgql@$G@QkpJ6YInOe2sN2LcdBsw_}F zV5KsbCWZ#Bub^IGaEssqS)%cPt6sI*6RtdF&7NL4udO;+S7@6GR$`O)-Z{_1}SLikJ1`hiS%TD03&m%>H*v>m)iZHkubyMvmQ&pov?E&Ic#En%!VkDkeMbEg}y?; z{QG7I5``<9qCrh@U16yZ4>Wr`UMPpxY99ALFVJJ$dm}ZAfqL+pHj0ZR5Kd@+xfz1; z$(8U0Pjwh${NssXMvURY)!x?Oj6^IzLIP232+wyaLi8Q^**DZ#1+<=kempYxKU=~y ze?3Chab%Jb|EnQFiwIr*cf)G1mk9l@Cot7qSVf?%2lPhKKLxA()gW&C1N!;D8s9bW z;Q!U_zjfvd6Ylsx>wmSC%Wy#8COi|_TXEBzQdP@LMd&8{cURy$MUr^47`Bx_7f!dR zF;)TdABJy8jP}Zj?cMSlxo^j%lGWE6xr_kS#+%Ho4KpXhk2}FE>_{*GlezcDp`ug9 z$1rcI5n57(7&?cHi~-=H=Rg+;j9djZwOZV`nc=e{c8jO zP*wVPy}{nN@N=1GfCxBrrWfWCg%WP}@?9*>-#`5GMVHA$$G>cI#_i8rS39X1ATI$6 zf|-Ke^_Sd&cKJ$2KZju)O$ao@ErPPP!8I4ZLNv))D&t*u>qBeBd2Zt);S2&e=v5|> z`K*6fk?IsGJ5L8t2mE%5LR^*jWXIvH1x?Lmjr(C(Y8lgG-&-_WgHh*lLi2{l&B)vQ ztE6Um%c+@?MN>BPX&n%Rij+NrDk*-IbY-zXIAK-Rl(SGBb4uzSP1mt=6*|A zCp0cHzHbc866V4J?Y4e#jF+MDZ_pq3dFR_$8^yDx@OSyj`Wm+dq7PMVq}J$hIeb>I zCu>`e{^V|}m1z9iwC28KJY|I#LfHbyX}hK!pIS~4Soc1TDZsl(D8-^clJUyl?#6~@ zUP4WbdJB)V$@RQ4NzAM~Cf0IPu8EkE;Q|{9=(yYH_JIQk*$TY(=em-839Enwa_}{m zG_v)V8H?pQU#=$46S{jBmrtgj352d`Oj!f62LGH%K-}Fqu*+Plg{b5iYg-If+>U zkntU*Xi%Q4hXmAHKSW!i9`dsuCjbCygPt~H>N~SMcU-zPUKX(oU(CH^?CUo34#KhE zczl2avHpK>t6H0x<}LBVkDcL~bkIq|`llFPe(GoGKYLoUF#zXR&MFO>h;RC4vKqLo zWr{fAch4~PQ@DmT{u-E1MsXQ@COR+A0@Kwc^MdU}IIx_A)*kn+AJcv={jy-;C9iv_ zh&7+r`TYJ=v6nFDwZE-k8gO)dj2HHU1DL3BH@*B6Yfkp-kk%xLtDB7C8a9yWQ0tEHiSoID6T$PM*`l``CZGB0Le-@>|942g79 zDcQcKC0xzGdOP;rT1S35>}+rs1LP4qB-DjVX_;Jfc-$p0Qh_$p;~dZDyvuV&0Y??k z*{otI$G>1;(Ph_NACXCY>CBs4cKUW|In#7VC-a2^$hphr$3#b&%fb{o*>psh#dEa$ zG%^2)^|{vndP-_TlM|?+huca|v6vud?H56@&bpg*K_KMD{?LB@rbPlD(c1P#iXy#_ zHq^I}aOd=bc^<)(th~RlZ$?YnOwdkF^`%&(Hl9LcP|Io;7#{tNI$1ltpz=l`92_tb zKdKOksN=Av6$6q?R#qOOES<2iD}grO^Xs}8g!k3=>ld!0PqQmc10(E7#km!{ZiQY0t+)AmA;Q+zFr|&` zz8<@70pJ1bX$H@CzEG`WxSKF>6HU6DG+utLxAQ|p0HKRZtjufRJAJYx5~nwHqGo4) z5`2gM?fefvSfisGD(9{BY}WLu8k4o1mH2T&pUd`lDyT#JtFpQ7uG*U0i7@>90rR=E z=*UZ{lj3t>P}0I2x2frRQgOgI1|T7U77!h^BJl~6LxloyosXZ%08#|nZab=8Qxo%0 zk=qwd%HpoXy5@p&c*+L_5+J)(X7v2rdz@Zu!9BF)Sm*wt2}IAuI+x}FiO2pIlRtb# zLqd3)kE<hoN$GsHqQkfrzZgrGNkC4345x;ItyozLC+QR$<~rra zf5!p{ks*^+geGOtUEq(4xikAr+=t9#Ljho5JIaX{jZ99rq0|)d8oZ2}xRV20gBE1< zZD}D!H`_q_o`~$H;036-YUx8uwjAi=@BtLKw# zgQnV^MOxXLhNO{+<_hofSv+O1TJujP#Uls1`T>}JkW11(cCfN)V$_7k>&p<%!u*`T z>vB!$dz1mcXLYNctp!ZuucR+T&mGaz996Yr3bQvm5#tykK>#Hs471X}&SD!DrlR;> zg-kVm8wWsmBYO+n^y0#y*BRSr5}1&iw=bF%8I6=*iz;~dAmrG!ZNeS7PIPE8#5?yv z;ZQYWC8N~%=%RHK9qW~?z?ec7znGm0v&hVj{xNlNmP~2zZzU^hqO{PU93yLuNYHfL zVJ`?&5=vXgNj5>lax?DFVLOEj7OB<%OrS@O{T@vI0Bzp{(C{jl0TQC~y^~P^->KAL zk$vN_6F+~Y!pqN+=?9bQNchB!11o<4iln;{!sp98hO{2CFMyL%V;SqD8lJb$FCX*j z?cMq3)ttENzMP6#-98)KT|qwa)WiKni~c81XG7aIg!mWupT)$P>_DMO!Ihm@xiTs@ zYC5wqD@No*-7h|lb)UhTrfQqHn3*GV(JK|`zv6NeFYorv87rok5>e1U;erMHBZMZ3A2Wf+ib=0gmczhIucaFYzz55g zf{q;3bbp7=vw(kw&P?pHNmlIEBs~2j0CW;?rVbTpBNqFe zqvP?+pA$%9?hJifx{o}jGAP~7o{wcIJYL-8^U0+>1qSqV;wc;7ZVy+QuY?6c({rG> z4GfEA`1K7_4z!#unDFu2y=*)#N?bkpRiPp?uLlfNKZGqGBw6l?gNT$g^3A|7W?;T^ zTF{VJHoYiV@fnes0S^o4#(L^(uRl{rCy^6@f@RE0I zAinog{v$v>1qH~XGl|mGG}4I@^#6>G&CrtMV>(du4Ze=4^~*v8iv|%-VmZODoy~t_ zb&Z_z!IW}T>`_XC<6aAGz5Wy6&@~uiCs-* z;Rt8X1(xmfhf47(GNZDLIngUPgf{c(LokQvxT2pBn@%A{P)<8#GXpep2Ey;F$8W3D zpk*@|N>U{i+;zQc`Pco7^CyCXn#S0Hg^)$w`%L~+#1>B~z(_y+QoA*Q;NvNSQ&>t z1tNVt)XROU?AseePTa`xxHYXD^V-ZVc$%3C;&LedPtS*%=*>(Za^-AfAw6lJ*p5rR zvl*FM_x{o3{d};nxXw{bbfpV;HlBEjq&=r=)t38jr29ThciV&-pZ*xHjrGDAwpI5! z{l|vruL~)1W1q=X4Of_XT@oRzR$7h^#--}Mtnls&d0d2cKCJee4qofB4Nb-Eb#haOef)ji#HuG>{ z9Cav?!zXt;9*?|~SkIT$+N@*$KUb})s4os1^$VvH z@(g>B31y53cP%R;?+GHVHmHn>X=L6P%c0!P`x#lbG$D97fhU1g^h4O#Vf0H9CVS=k zkyUg#Bh1XlSI?HkLZY38Z&}=xT+YcsBO)%h5>&{=0MPNRY=n;;(uT%{2`s>@X|n^r z2v9ITyMi}P%YlCIEs6{}`}~|C)}Pu`%~V-Bhs&dvWoh28Pb3HRm0>O#1r%oGe~MYS zSZNG!2*axIgjuQ#OJ>x;$q2=Ke?$V8e7`rlFtO}sfD&EI@AiulyubIxyiyEI__q+$ zA*Net9DDDgwm|;u^yS@~^RJj}9PXf~z`S&XT7j~r*k5$s_uAEf>uhYLp-_v7QNiN?@5Q`~1+T$|!yDHv`zneWZ8+wrgmXs2km zaleqVsAqVS{rfsA8>vEK{ty1N&&pKG`+TB!Z;gViOYf0Dk*~4|Xf_qG=sIZ?A{`QW z8)>;3hzdJq>|#gP-8u^Z7Tg_XegV@(#%MR9}TU}r-G)F z{qDUCB_z#^b*MogfK5t!Vwps5&2_1gy!`ld;kOr6?vnA7wE5K2sT@Qx&h_=yKnHrt^QNumAG`H661YcVyUo`$WT=z-$WBT8YpU?pkwLhvmkC13|#Rp9t z-W-o+6#d^FPsFN93_fl-yDaSAdwDTK$x5saIJ?`aL%{OtkuR5d)@rjmfaZqF2-2@R z=~PKF#UjNf7loHs_vEWVc8eLH%Ad2X)8~V`naG*#Pm2zkh#$Mztex(;?NKG)B%7y9 zzMiCi1dW$nH}B(AJ9anV5omOcTWVX2{Zp~b>Ky5DzoPKRuwGABxVr<#TI1v*SCBx(Y1(&m;GPuMSqJXgQF?TN2=<|FNeV~XoUZG z3uOP)PI3fi%|}wp8dMrQ!|b|<*dtVYN2%0#&>wZO5fAh%zh&J}&G|j4d}Mm(Rvy1g z|M++);ZDJtl73?$GSUF|TrN(77_USAQZO#)!xJ%HOTQGe$@lag&FdceX^kgSklXci z;S!vL{FV6ex}f)dM^lp3re)Cz5NF9gKvLBvSkO$K0Rjbfb44>wdHB%4!w!%u1)`sTs0-Cx$Fz+mbiyxCw{m znRbP(JmJ_irs5#{i-7gbR=2u%!%nII!e=flHcu!?6D3Qy#RcL-Y1w;ZK#v5P<7!5K zYyj2ga)@s1qX;Gn#9@&OcCV407Z6mZA#b6deR4V^;AjAb-(6NmYR|}^h0t!RWjw1< zlOSP61ef6f0G221FV6Ja;y*^$AFm&puPps<&WAZr07CKphfS#*ZHliuvl^UGfP(Q2 zk8i`F?AXu(`_>L^w&~eq|C2rx2L?zh2-P&G<@l;@-4tYAZ}2z_%Jg4x-OKg!vu84H zg)tuCU)m+RsMI#-1luuyMBJnr4s>VcM z4)NG$`+hj(y0g@Io{XCjdh8&v(bJbV-3=P?KS>uB_yL#cpYwgx5FEoWQ6C=L-oTg4&{t;oIm^0nqKCT;ST6~M^T3rpSrx`uxt#T zr(@CU0YtijuY-~~2#SPd6AQCK^P??WovfB4^?-2=Qfr6z*Vk3e`=z~g?5fSw6P_FU zO3R#j2}xYV&GIJ;SJ&A^)=sBapwu@$*7D4c*Gc`59Zi(4k0afk-b1|EmCetC-D_OW zJF2~HXShoi*vSQ@?R)DEjB*nN(JhR#c3#KH!NnBWLBs$D-v`DmZ~$^aYoO-^sOmvZ8S4xj*s4+a@F~CA$Ox8Ic=bxZrV`;qS#$)h)#86e7~_}s_!y` zLazA{b?cy4a1N;n-QlISH{bi=LYKI<-${Vc2;*FZr}iaeRko?Z2Y+!UsUZL$Fqoui z!G^tVWCYqW8O0sAqZF+9|UYr_$Y`U7!R5d?lj#l2Yb52OKqE0~{p_5sy5epCK zp`9G0-iL3J7(~{FZGR=rW>9?0q3CRp5R2DNO&#~*scjo-Pm*WWv|o^14F&mYiF`DC zh25-f_s=3$ev6svbh5L1Rn{&kfC;H*wUp?wzthU{Po-4mv|6%GuOxgP=QEV0xg*b< zkYFE21&mZ|d;$QR@*XD2WP`|L&_9;L{q-gp{ba!bKifo{4Z0)>NmQ83PTIYdJi9hO z6^xaSGuddRSZYHz;>yee09HlQbF1vE@xKm~t4bTzG&`+D_d|$J^bU;WBW^^nQq@%kJhaa&iD}n#^;=v8c5@QaxTc@*{I;c3K z^`McBrVw-b4*yO6&H>|wb=SIp5!~kp5?0W5+48=HN#I>!e3Hoe z&aX_uxz!Ab3Y3mH0!h#`qbeSuFZ$cw*MDGf4Bqc!@C=8GO;d3hx9Waj|G}+aHU$78 zqbm5#_b0NVj1EGOaCKjZ9ZpIRo_d=pG67d7vbU8SwyQa~-0sH}oVE52@}KALj>3SA z__!+coaa%~xpc{)h8CfPGe&~Aw=2P6Vh%VvM`E^U7K8tBHfze6$+*?FXmS%fT@aq@@ah(0 zSP^l~swvpOTz&E5hJLfvU+?Sk{?(LVeAu`Y?Fh2unlGz@m_%ZBLP`m+98$7njRO_c z^|?xzWU~P}M(-I2h>0_v>(p8d&ha~IfrBF;- zF`J`qgdSZy!)cO`j$~#v#BNYCk4UFeC<;d)@DB9m+60qTt2rOg67MIYU z;GLm7`XPsH@#p9pRO1l5{DM7Utc-^~4sNs8S+GH=XaJh3AL13f>2gl7kl-U{o(KCEavH*IZA5 zR6^ga(kV`{H4WA`P)wVqVlCrF&9)uo>O>-vd^5f%^PhlflM_Fj`exX&^!2J$PsCQk zdq^;Lmq}BrondLo&(*AEHZTFcO;~=_&a6v!bjzD7A%jjHcB3-T`D_B^V}SBZbq!`C zMOK|)-|#sCWN>fmmy>9ERl|855x5FSHyu49*asnr8A4NFb+YEs?;9%60zniVXh8Xu zU~2_wo@zM=5+GxiM*%y{X#o{so?&J3c9+1leLJhIZ8r}fup#TbtZa{TLT}+Ih&YA=yl-Y= zzJ$WUx!W%4P8%F`WM^!zYQ>e1|1Jjn>6x70(MI_%+IGaT7|W9VM%m%h;;bTWFaXfm zZ)!WH`_iJ8!@qu7jLf+wen*53UI+}BOszLnbZzZA3+w*1Obgy)gz9@a10pmF-rEa@ zoCXu8{s%t-AGck|u+YMlYbkyg{@}^^O846t%I7x|BsJE48SDS3^h*W#{udSgB1Zt% zVe9)DUJEFRrl&Y#zSs50&oHYeqn@3~;sMs(!RRsV46c+F7J!WAp(zdDE(yWYEzEsYXacW#Fr=gSW^Ov z^}rQ;t1q${o_8NKaLLd*YUHFU9?4=`Og*Y-B*@eA2owh$4~AZ%6(stuI<Zf=eDo)p8o9SVfzXfotNY<|9A0JD}Q$v}yMpj~%f@&GNl zJUT%X+b_PGLzcKeP4m*jr>hO~A0qJsf7I2LYtnUUTjb?GC+b)8PnDirU$0oyVhQdQ z9}BO{E^c<4bq9`X7E1eA$3%P*ARq4$UUvk!CDPMy>Z68P0Rg8Srs*A;1;h^Fs0R6f z@PREGWz=TpJ{6Q|iT*LL4_}3IrLU^0M|vm1|SW46!gDq(GAdmZOD)3&LnaLqXIX31m|5#XGj?x|rQf9)dC)$6agXo0DD|gR5{cr|_Q3xaDCEs%rH> zU2bZcvqSUwrkSjEp@P`#v%EfQLCv=$1?X}|Z~yX)fKXp!rLslXfL6aLGth7{E!$y| zl23_*LN-2s+M_FjiHcwqL6r$Yy~Mj@y3)&u{I;Zdu}Glx>wq;+dUeh!F$tg z%u=tGPjlyHXmhQ3${a<|$;|aO5_l;?uxn-2ro@Jtl_S~zRgKTI>SfCP#G)9zi9%;P z9$B@~^pCnQt90|t$6NMzp>)nYn{}u2)me=Vce3}Ehsn*!+NW;+3PEV0BpGxu@Q10# zj7`UV;MJvhhk)aWELZ?+U4*na8wHGuwL1VnQT?tv?ZAm>_;ToRAw@(J^z~$Z8iQOF z^#{)(4E5ALanyLH)10BN3uYO^wgxX>_lqx@p_{1D~yZb{E3;90i%ayy`||iz6Uko z7~?<*OUNuCb7pAxVY!DOwjKNAu>=^ieIVD9O0U}nDudK0^+W|tgeU#^w!w#5*_adj zK#_-f^kl<%h%r|R6`ZpToTV?_QwagIvgdV+PRcn$avM{7M-&`<%w02Fkd&|~mA3ao zk3~lcH>l-t+Ft2E(0Y~u)AT(Y0AH(yng+k-LC$&SP4sU(Nf6G-%(kxg z<`2Qcv{)gjouy!HmDgyNJ<*#fS{ad;aJll(nq|)P4lp&nleW`l1A{qLWB0S2Air0$ zT}L}M#R{g=tQ40Fcashj6StURBMv)CwlTgcaoUig|( zPx_i3G*t4wSM!5Frh0LR_JFf96XdujpCX#Cw2HP==+zo`x{9(eGF7EgJpj%gvai&v zQY@49XCA+}HhAC7giGl6ZO^zuRt$M3I?kqaCORaSeccVN!;Q!rbOyq6lYeOJVo&j&rWAXw7yRe=_NporE9E&5I^7*0TzwOVE{ zgZ}l&8WND|k{;K|m( z(0yG;sSjkywRF&-*%*~0I7wp>6|+AXWs33Jt_I zCe{drqb*tRHn)Au*5;G0N>r#wp0v7R6IpJ7hWw}UA4hLyD1|Ekg4hd`95)=ym?8?= z-xQdnw{6WimwaY6`Ic$$N&p!K#6&yvYkw*e0fJzvPr5gI>r_vMrqalE&k-7m*a8AgW#Us+O((0LGT#r zb0yMEJ7%Lg&!RnEOe>A%iE@{mO-h>o%&zFroJ#;tGbiJ2t5^Kx@N{(iJ1aU+-MVCx zhtTz|WmyERZ0CM(s3|Om_ma!K*q5w?p=z^k>$KJ7*QDr|l8zAqvt*oV>d8D3A%uOG z&i*g(TW>UiCd+PznT!_qZ`Y-gHIq1a7HBll3WdxWu6Wz3bF2*oe*R2ok)z`}q~-?w z4SRKrh+A06U_v;pe(ySW&YzC$T@ADJ4hF1PSsR4&r>V6X8cNjIFIUz$DG1K!&pjxz zM5^bl<*0E)(5R1&u$_7cVZ#!=4%0oh0_>K-GTkp$4?Dji)ZtWft+JrzeZG(0nv#yk zFYIruXHpfgv<1GDP!Ac9&> zF>I((jyR!M#t6X!+E#D9eIDmQFMAg2jV)B8ScSuqB<8D z$z|BSh>~F3BVaN$u5WtH+1k$nkOFG~ke39J%$}!Ubqet2=K8XsYV=;iDvW;l&DE4s zs!UBG7cDcD>rlW#Ih)`#gBwg@>KADBAQKgo?Atr)t~Y5|K^7A|pf*%*ceKj9c9!{` z-acp}jpK?KGOw8_JKlR79;lJ3UzWz~(H_33h{0^OXJaZ_=sfj6^q-LSx@<~rkcj;& zD))M?$Ms>e&yypf2?-at+|uq)Uf5GF{*zJ6s|3$CRCuqn4^v|I1l;*JQzGkK%lV#> zK(fBL)TgW88`IBW5^09@F8(pE_=sZhS;ulDQtH*4PrC~07wx%Czo-CGg-eP)YOhU} z4-QAqlY_!2We@w)NUYOmtG9jm^0ouBPIo&GdY=rk+v3sxM^kdx>yqTxnJ+Vo_G@jM zWy)7r= z9N+s_BL=`$e>```wn%{lTngW%8rRi#)l!PYg4r&`F0AKq{dOE3fA38QfRIaaZAY_C zWC*`0DgA-}%}RCt5&5SdhlX}{aI7coC%<*u0alH7h96HJvIVMq>AvP`$t{W%h@fK1 zLz)X!XgJn<>pZA=5r0jK(Y*Y34%`((l`{J?81naLG>P<#!7#cj3jb+I7apEWQ_sPY zKX@_m19|fs8++;4N=m6VGu30&Q?pb;m(QPSVp~hC(F6^J6I@HOs?eYmt>oajhrP}p zOD%OH{+pv(da5+xEvH6;xqTq1lR~^#SvovKPgqxF+S~jBe~rmeHgKi#80%2Bm{wnV3Ml%k`D@pu=o%zW#%6H3m}Pf+h23F74PTB|V- zyn~5gU&267FCBuAH%FiV#8m+!9x8@xM#cU*YO^iyu1=>nM6zm9htr8kI(!FL&iJYJ zIX0SXRX1{QSIfoiS56)|F^|~^$=9clqdjpkct}ZIy!bn8)&8Czhi|*c|03$?vRWgm zL0jT&go`lf0I9bedALWn@cfLh866P52DzNy%*%LgmtwDmH~lXDz&;j6{RQp=Rx=$Q zQ!NUZL}&@5z5g_Ew(tr(z9>HLd*l;XCUtj5ax^tuFiyfIGm>W|f^5yP6ov;O^EFxg znXrGVImBS-k@TN{*+cn>n$HJ*d(J6272p%C1O9va57+|dk}&y_NxNKcvvpeOJkh}@ zdcOi^Ac|#n&T{b_+F7a0fhu#8S%F{P;#g8QiJf5GfYpS{}{7vTUOG`eK9v94msb@YXg8bTOr^k(A2Z|@- zN?YyK6D)#NDjSAfK0_&;(_O9pTlx5F_ceyagf=IaQsbHu-;uo*9Ff`qh7@|op`qE{ znwVxMXhe|sG5IC5rGmKd)$51pX%D`4lNc`TTuHD%Y^r5oA^CFzAyK zg`38im;fztt9KN;e^F4`^}485rwuoK7$84s*CNr>zNo{&XX-0RCQ0^(cq@1*6UCZ13bLI>SiUtG6WLn(nN zsYOR>$c|dx3b{|aa0h0;M9qh^JP*^WGo9o*n1GHar!T259f=Vr<`%yF&DrRl>DH)N zC*3~V4XSTo*nqTC2#43el4A0v<}R!%O2Myc3m042;&hs^ua9F=XaFJgpUvBK?8U{{ z5(uQvS;0#ni2rLXbGP4jO6&gdp8hW62wDTH!OalFs5{8HwEsL(e*xXK8O+O3HOh3! ztm!>(K7RVkeiH|b8Q5`~OGs-5Ce-Zr_#>7NH%CAzy}U56B+;SUo6??13^d}O_Y*-k z2@0-aWW7=qzE}FIU-%yw#U_>EQ@mJ6z;kP0`7|K2PQT@4%dx z9TY%lxh}K%$f2ros)K)+^!*pV$CfW!#AJ2#46iQl#j3LI+n+5vytk`zqmBD$n}#Tv z+4&8p^A^k!OLqo`^8Fz*d!Hxowa12T&j-^?7(m{L;O(j`?gr)il~rXX1BGEp*>>_$ zrMynEPPI{b73O;#EI`Oj|EV-L7dOg~r6A8ieat2~k4#ADz1IQ&h#Q^GNV8!9^i0Vd zp+f)y(2U;mG4GQ`Ty^rmpTyyuMT00Sib_c)4DaKvyp1KqKTNVuPfb21`Qy8EWOJvA zo4hJWjqVb*5SY&TPEiNEc-nmzD>=*zu|9|QfJ&S?z5PiyUE-Bg-%~s<(|~VwgEAv}R>$SAn~u)n zbzRKVY-uU^e)N1RYO@-HP@aP8K6H?t?iAT*YKLjhrf&th*X_yMO~aYL#Y^O`+bN4r ztRsWYEWP+1nh9o;x}T4oGo>Cz8xE!$5ujBnb5{(i>l@vkDQPe%%_)lSkqkF=G0wQ& zR}OOBz2|`!%hnpq=P}bm5XoHZ;K+Iqll-0893#|YEX5caSDnKG-RoSZjWJPNsC=^+ z9X{LVt`uu$G4WTjG}0UOHX;}At^eT#@a{bv^micq@+H|5)|75V-5lp@E4TADTJ*9* zOEX7v%LZ(k^>tcJX3L8u8ila$&K~X45S<-9J-vvA{QmOQ76HFo!_#%OEUHdkS2ge$M(|e|0*tWnBK!@hRUI4sPE5X8)#I`NV4%Y@n@Ock*<= z3hnQ^YtsF5gfvCMr}=y{rleVzyM@H&b^<9jx7(%i$>}A4o{X&RBR*nLF$0gRpco2e zKeuhflehKI2pSTrTeR*;sv!&>yn0T!B%X$E;)S$`QttVcOfHWtJLSi2lD7#3?Y5B( z1ww0DCJF$cAo@`y!+<$jC%k`)HCr-wFfp@D)-dhLJ%J9a8NX{X#7{p#{7DvUuaxM9 zTSw+d8h|W9yY{%lpP;OUZaUrrPUdg*LlIl~oTHZmtos15F6;m!}sWt;k3oeU|e5@iF)t zbt$8=oZ|nUN|^u85QgRGk-b2~XaVQvlZxAEIm1%R+a)HZw>SVxt^%oT&D-tVz_7^P z8c7u}q}I$py80~sv5VLHVvhj8fqT>gBjkb?h7Da=LtWss?^5Y8hXY8Dq6bI}*%k&j zNLHcV3mE?w!+(06%`1VP8e#N~%H+}SHnyc}_daSnc|w9#{5*gWLP@Hhuc2_wbtq^3 zYjH;EX_w2f+}_EH!RFizV+uQCwP`K>TchV6G<`MDrK10p4k=5*HWPsbpkkN71PePY z=>>)hJwtsfAJLrqT9Nn^W=sMIU@UL6x!E;|f34+}Ylj5P;F>!vjm4JsC=Of5)1wYo z5op;dnD_8YN*eqv`u|~~5wQ~QBVzZguRXb}g=S1$I@?ltd7R%{v+jC)ig(U728p5c zEdfZ)cRC@S?tcsI45;->M}<)$<&fdvx>{+aw=EpnCf|)=00G^@f7CVh&6x2!L5lO* zNudy_vha0#HmVREg;GkcmY>3{CvL8tmSJ*s!NIV5Dm^TeG#`|PdXh~E@I)19O4zOG zo-4OzeB3g#)8sWty%n3nx_c2NFA*K*^nN5U_4OBfhHG{bzW&)8T&uvG&;!DFjwm{L z#yyZIZei|vUFspCw_Kz90vOKd=Zl%>fNCcWAesZNGw^Au7M?UImSS~(!rYP+tDODm z)FXI+2EXDt%;FgCL;(`i~iPuW!84 zE$}!jBmcTlgshDE@>C!oxWVx zXtTQQucycZ8{MTM{QPUNp(}4Coge`dP(p!rMnjkt8ykMd)Z@@~Dh4(cG(n* z-^=uy7s7i2e%*K&syiz2v5P0x3`<-J`3I2=5VNn>Tf=I!9=?a^m&@M=FX7|2yxI8X zhFndJPBR70xOZk)HROW41*)Ah*69NiDiWEgOhMk+KRHk6+$20p(cbt(F zYbaj7+smzBDPw{du}4nntdY@m{Xl7|I!@y0oYmG(i2CuRLkh-4Ics(43^r@*1k@6| zrsefGWhD;JQBYm-xuo*yALX-AZI0H3UNM@-^HLXgeyvp!xExnx(Zfh8x9{l7I}c)B z4gOuJS6Bq>6yuefN+DM>O5W*cQZ==^LvOR~aHGLt>{IoYFHfIs4NwDB zi;K+Fla|rhOF)B!#8*|z4zQ^Nt1aqGeTAPM>R;Txm`*YT=nqxV%_lA3cPFcx=GWxd`PI3XOz)`?q*omo?*EhywdkphPcjQ;S|)11u@*O%xOfm;l!5Pb7TjY$#w#;M33MZt(g|& ziAHM?6)`ANZwVVcg?DU% z=7LrETvTc7N7Hm!iTO9t30e0FI{(2$hR8dbHYyg{DI3N z-~a^9&i7~C8s3!ZEftHnZw$Kg`7JR7Ut`JZEsr~IuXEFDYs!~B6`^n)^|)_(Pioq` ziRE{rir~)QH(Mf~4VxIefBVxx%=+%PF!};nZl@HVgoyBFp@U639ip3o8qU0_Ph)2t)z;dr`(VXdq{S)4DNga?6nA%bx8g1>P~067io3g}l;ZAAaF^mv zZo2ok&v))U-x=fnmywaRVr$KJzH|QO^DwvF>}Fmd(?4#EU^2sWcnru`_aygcb(8KP*C;&)SC9$TqNy`~-x zAov8G^xd$}Z-&fN&bJ>YdKG&aFT;1 zS^S-?o058jhxE0ZBdF~?ll5xYMlmmF3@$V>Qc5OqDzqvbg)YU1=D`W9On;90S`Djb z;UH(KHJ2)>1bn_+GvXMbbC%gPN48O@zpptHLm83!M`geo+3P^fTae5M0S8pACrZA1MSZ5q8|`e+d-)rvpg-88oT?`Y(Zk4EKN34yOJ~?O?Hs z%xy7_u=6RF0eZM(ZN24Qg!t#&d+rY^$R*_`6Yn(z!1CRzC&5&~ABb?v)TF5rF;x;H z**ja51lSec{CO=coZ$|cx<*`UFBZSEAno!=5*5|K{fJO@^Qi|3$%a`Il~<&*JB!jt zlp&_*pHD9HV;EB2$=6A z7Q?Q1)?o=|E}`ca6I9UEKf`c5_HNGs4-n?tca!1^PJ;t?fMhK1Rtgp zXGmVfm&>CY%;y|4p>~#>twUU0U%gG0Y7%U`w4Kd>+^XI(*^5I&%40;Q|8p zR0GbxK5k2-Cm_L7BSX1idY7XCPoJ-fSYhIn=AM^lM?g!RYd@nlqBPbn-alFs<8rT5 zaCefbj{DoVV}-nny~jRRxl9xL*_U{Q|GLmKch$1$wNO*zuh&}jFIhj{3_{71YA6<6 z$JUjhl9rJuGnO=arJQ~(n4s(1a5r~)r$=GSVLOFTGylV+1eV4QFBM#Le6DuLj!xa1 zxscClYSK-Tzyv$ztd=wCCETp2Y?@*YG&4j)FJp|@Chk7~ln!^*G1UiM=@-sP%-ebi zv}7hzaRZtf9)t`lYwHI2xVE9)9#rcARZq*99BYfvzH#CxyA_0jn_wN7aRlEBK+@@k zH+TN5DyPpuBDhuH6klkB-=lfIC(nVHd%s-b&n41&WnmCESlp1=xFyuCw(44bcWUTX z9~H$42MFxfP@@vHUn%)fEi|O6IXZ`2+WFCGa)!9Pp&@oL$-DJ=H&GzMYM470U877Y z6@A9CO@RXj*F~S?$Tn&vCwZl@lP$n>4hOhqB2Hj3EH3t0F(3L((47t&ujd>TY6lCkT8`wXTn-u!< zmHpS1R)k!EN(7b`p<#uUhCELQNtXx2-v8L-Lol%P&zH(rwOCG5Q)&Nvy=kjfL8|X} z|MVHAk&?^pK#QlC?RNJ~+%#$;jxL&u%ZFx9AqvS<(0_|mTyiYlqSpVWoz(ur4y|O= z1y#$PPRQ7k_>h>WOwaEcm(GctEkt5BZ(9mKsN&OVsY60`MZAIu^7PW|)5Fu%=`Bnd zQLTL?_^a9<9Tbk0k!FyZ_tlE5Pesa#?x3Oex`1c%@fCX~Ncr?$b+;%b1gL00QWMyp z&$cEKe|RW~BkCyV(P{5C`t?xOV4jG_rTi*H^m?arKE62lHOl8s)Eh<#dyq6Ks5iLl zGm;vLM2XnwAi(5vBcX_c++r*F!!xxFH2FIFspR6?!fFY18BsK_ zVo7scXef9rU<&HauJ3f8+$7k0FjKQ-s0Yb1B~yO=IC#byN4q`eXT|J%o!DBJ&7G=Y zCe&(r%}!Uni|Gy&n^X$-=m(9nN)En32^T9qPue2Ubw1Jk z?qpCa_;8u5$_2Nn`X|tips=5~1qj7EwvV4*6e$FpvbMm(@C#O;LX0fVfVRd0{%kztIffVoSB}Qe!Cb>Jtm9pF^ZOmc^$t(7X)nJv(}MH&>i=o)gICH_#p>mx38tP9vS2nVL@C z+WJ*=mRy=p@#$h&lI`cIAsWUj7i#eZ#qv2iLW#-*^R2>FFjvp~L<7{f`X(gc7a==i zf4gP82Hv_?2_xT9wMk8iBig0^azu4`*Y{zd`KsEF`sv$pR#u|CuO*aGG7Rj*>gE?y zc*T^dncd20tO_`Afz{%;DFxEC?QdsK-^`Rq->hUeNgOQR7_L-&4S!QI;5uglquHs0ap8aXeFs1w?d$0Zh8MiG+2j)T9j9z69@0M zH|%g+9;=&6OoOWdVL_W!q))#DSEhU~u)Su1E$5L}a}LS|SexewweXEMq|>(GTZ1O3 z##`-#Mt=otE%3*Ujg5+eQAk4O!D*}KG;aE$T6Bk{^x{2_xbISU-s;lb#h@x(&3T8{ zo$tl)FOK(8iEPl5`n{Ti#cP&@a6j!H@SuYxaijy%ku(Wi%g4=3^qC{iNYpkuA< z5eM_!%Ghyj=wQ(180p97jUxT7OsA38_UnVu7gN0@qGzeAjI}@8)gk@z$=E$vU(_+T z(n5toq@$%WT<|jG2261&8Ms*nb=&LesXSyR6*|=A@dd4#e|I(9YX}<9 zRYx3+6hz`jtKSgFPmG@TT;P~$@M@Q`@>w28S zKd5e>(i+y;y63b6q(-&=2AzwW&-tCGQgodI50>{N>kE+}Sw4M>RQp>^z~Y_da*n>L zG4_coy{{_r6PiP*OmnZel%1clfZLHfbbVGHT7CoEgS%rXD^?=!&D-&$3z?p629{0k zm>4^6HMQaAjIuc1Snu>sZhLI+SPu2toozx?U(~cpX6s!&a*Z^1A+$0RPGRuY$EVsr$k{==KOp}|#Np70{58vOp~6!qKYwIm%n zdoAa4O6eMbxHuBCOiS2@*=sRiW(LrugZt&GA=Le9pnLRplF+Z^ECFzob+*HSJb&;n$xZMs@XbXGMWzMh)9iE@|G&&1Kcl1l;^eR>asW z(lR(nZlfLK5;2wLj~x`&5!by^Q+-GJ#9z)uTr=t^n(*_uXc+oN%5G&HiHNpaXvdVX z^4hW1zwv9ZhyFB69#i(J?pW$;VdgZvl|9~11jSeV3|DhZK|O9Pln9{pMxPofW#1{z zlOF}mr$u@(%iOVMbA*ZI8263sBK`z?!f{lo{;&$Ff;vsl?cg&G6zY0;~`e5ou_rU!Io>coZ_h)Tk-q%hZ8Yoe=H)JeUf5>s3twpKjITqNTr? z{@{@r*T$vqtwJ{`v*azJ!sq8nfg$Q52>(^OKCF{R$PEGKFRNZXc44p}sY~}jST0FF zzSgbg1<%uNKb~aNaelM)OSIYz^IqkC7eEGfhgPi?6W-&)l&&}yXuM&9DF;9cXvhP|vUP>9Op<*E4aM2-FXus(lxbCz{h?|(>FA$w1 z!EG+?R}EZEG4Ho2Pa0R^VDq%Zxnq^EeI1GvU(`LM;jo~T#^9#$uJ#V*w8h0~y6!Ey z=PK&<2AND+SBP(DMTbwMT3ZKTT0l3P1igr|IxzECZw2d8z|e)BDzZOEbrW zou)1R*IbdEbx)7hkTBceGcUX6qf1}$o8$>U8Zl$m-DDbSYH0%IokOPv{gbochsv&~ zS9Ot-C;%nPxjR+8opV>0TEw5A)|(~Z;$@FV^@@D$unO+Sqbp76S(U_;;H6luCSEZB zAf3KrIVq!L)=G=wcaDR{p;sn*oYXDXCx;K|+Cy{HuWUn*AT!)Lc2AtD30X<(+&yL~ zvf)@qN7jO;PTxZ9Smuc2b05;CC>5Ejix;>c)y;~PGA!Ihy3_Qy2ouGSgJ$<$5}fzt zJa1CgZtq?u)L+#x35;B==eIBLR%Bo!be+h~?p)7TlSFA}zyGif+RR4_l)9{&x`=2X z!lk@@oMl*F*Sr?lzA+BucJ*UoRi>t^iyNSY|$fTg{+onPArJO5h+NqDlEun1 zZxyCRo080u;s)JjFZ1|*HKToEu-W1HJh=~G-NCohGt60w0edl>83>1~lqHmLL}2fV z@fZ<4^k9zp?_vCom`>nadguG`8vH!B&Wvrfb|6_bFKFqCege%@&%p%wRDqBIMDETV zJcN#PN{|-lLm7sP%+5yJ36R~gj~S@+bRBsr{)%StT@FAv3FfeuX`ga`^Lz-K9HN~| zXY36GS9Mvc-7TTl)>AHG6E#meiGrFU^W@~vN z8l?l?Q&>ze(AxoP5xy?SXfKdx?5HsQ9V3E+l=AbP=0o@nEu}1-Jd42?+G!IH1?s!N zVwIUmtMN@j7X93(J@tn$u=rC&A0>&HjZQ`)VV!>G9zJZOVV(Rl8f#j_Zg4A0eIpv% zRD0Bsddgngh(0;s`+92%`|;~=ITO9ORDsq9;X)QIVBzseDU<^zFG{XRMeO$1my_`8 zm%@`%5y_xg)e+;k%$-7fM}lNcMuy~?1Xc;r*D2v{+`)|01Fee+r@xmIEVOqjCVPll z51AzpL+I@tbnag4Z;R?1U<5ihCcc6L$|a_1#Fa2;kz-fr&)|!u2Olp$C;+ry1g!>>ON{k8vv9m$O;=`8Tpzbu6!L;fARgkE3>A(!QC=LY;iek_`t4 zt0l8dfz0`LHl(LgC0lHj*NZ`HYhQ$B-|XG<*LeEGWYXs{jxrk-zvOhCs}~b??ANwZ zVO=|boCe?VSB6)1g`cB|73Gz1T3;vd=i#*qxx9E2P#x_Pvg$-AKTn|yW}?L_xgy%g z{Png#?4WnV#m_p%MUgO{iIXMX6IAy0eoefh$mV+beSn+KDvN)H*NM|qq~uDYH2>G{ zg6u3(=F^Mvy62<@^p3oNiC(zW^+0N##gs3qimZ-$K-76oZoxms+{7pJj+(_ zACP$UZ7(Xc&%oSVfpX=M)pkZ};YC7!=o_u`>{JOph$_}+}- z>X;Kvc0wfb3vRdhe!8YlE1x9zGga`*(~M(*P;k-u7x(pq;jB?SM*|Dq%}Sm_lEn*Q zsp{k45p0CO?{o5Md~EC&W-*J1ZS~dq{vZs0m!3C1% z%$fpMi~%U#eo@{&_xGvIT%AG~5$2gA9fGuw=QOY}g9zO!njC(z$ zm&kljdS5;?0w+9vXgOoZs>?>gSg-MrpK1Abx4Xcm9n z5szY}&I<_GDCrD$qs0C^B}a$k>{o{vZYM6zHa!a@nvs+6fq~f00cpTiTgkIdzAvYp zOOJ~^Yg>zVU~gDiiMof_Qm9kHc^!CA6no2`$6_PFJF1i@1+b4hZqR_B!DsZZ_omEfXh^bT z@g)e;j($*=DQKPb1ZBab*409+PN<|{F1Vo$+>OVT9|dvl$ty{Jb+Oiz0XPJOtYkA-C>cWa$1F^Qb@)Bj8?K{-)Z(FzPNh4MbU`^1;K;**pP2wcWIi3uF>xq&3T#Q0?PGO$b=^#ciF{UuY zG;Q6`!Qu}0t6GdLc@ExI2B^<`lhrZHIv!R{|1~|F%ctEJN|35FPHZ$xgJk_m1rTSW zG8qPy3GSy>5j?t*9md1mKk=KVvPn#M_284GD&Q*^|ECTyl2S+OZlqs?>T5a}; z$);}+$qN;RNmpy9NZ19T9Am1yAKpDo(E^&ep;4npX;d1Kr#O|7oT7D{vB`WF&s7$b zAT0R;mnIhGb#WjeA{m=#!Fp5^8RA7Z0Ce{dminDC8Mzlt9MLny($c%1U9j4C0y&M9 zcy8Px#>7R2fqXw$J}LA@p{wlAc(5Ed=(s!51(=82tgScBjopU2N+v8{6)4)LYLZyr ziOY{;<4MchD|QwTYn5uqko}_O52Z0i3y)CJb&~oe&Ept1nEaYY*irUV_ww5Jq}3&< zjfbM=!W8;|d*_PX%|VcIRUDFoR2X6O`Hx!Xfqb19)(_UUH@-fZo%Rf0E$BiJI{WoL_#Ikx4%engYMtHfKg2UV1Y6vfw0 z#R~~AJ+~HN$60v5i##ShSS=Ry@|ds)9v?x1%VvxMpHN^tCPDQLcKp8KMwTmAD_W8*A0MmQ&d20eMi0#ca;rf%w~u}_}qY`JU# z>!f%O1Kw3OV%d}d%5}_sV!u{cH21*f3#zy=4z7IPA`ZCC6CVFks+Y1xd;Ar~59zeo zRfZI93F;P1a=G&Ek{PVvd4287-@+>&HTQ6ml;N2Yr1s}s5H||%3umH(e@U}%w<#y*K7qv~_m6#jvCTjL|ZVyIF>vEexz+%VX$>%yDH_#6hw*(8lI5aZn zSGyV6?c)X?Ahou#ce1+h--N9Fj`RDv5IziUqjPkz&j|S}&G4ZGnuoe$PdbSK3dGpA z6&=V0$zB6PsmBAf-;@~0o9O?Xmcz1C*&Wl)zx42SV>qdvZifi+*Cx_#Bi=7@h3KJ) zPzBLxzj@1A;A&v8wM=T@-PCx4LqNmdMKfob)zaO5cu-WOFW~;caU!&>>2%RH7)Gpj zDLP)dLBlF?W0Znz=7Yv$RaX65>1?Fw(lS-oDyj|%W;UuL%0nbd-s7;Qp`CZkM0fk( z7nBDJ2RsbPmR(NU^b1D@Qnt5tZyKodTNrVx^EVi(;-r~^?8GC3^; zJ0_>)JY>U3oj@v;i`S9bP8vXB!F6S#vJ#whZmiY~breMM6<@TOcjuW%wiRn<$F_5dqNpB|9s6tuKKU&jFPMa^_ zwSdpqQ-=i86HWeUTI3N)ddZx}F!9_lDSc$o&i1<>q+3ee$+T^nj(S2&?&mhJy1P>? z0^i`rI%r?hs5-h?Y^k8!w>&TY+)90K`4U87Z;$T8D;h#Qsy&qk5TgdFk;zha(laCt zbJE;6_GCm4P5x??fT~>zNUNfIFe*1?^Fz;bY)^)HmWF1!C&vz)ryeMoa{FtFOQviy zhs7oGZJvrTyJgH(h|Z8-&=6WCjk3lJbAj+_l}pu3chw&YP}e#}iXi^G<+{cLTvsGI zQC^gA9-g-W@wX7tW67iiy8U$ekz!gDttvPP)RV!mfxe#W! zg5|U__^B7nJMe+=_j-^votXWE!iwcycx}H*8n&&##4*W)Rv3A#zS>@XmZg?G=C%K~ zcy&T{svL~eDZJnF@b7egtQ@(En@h-^&hc7Tzh|W9l$QS7so<{9xl6Ni@hU94RBQee zSw6#iElABm15;z?_KqKsGF7IpJaKb-E7+@Gd^@>Bq=8E^SR%8PPh_#ZB@ev(5+M9i z1{WJP1^30}5|Webk<%Wk((tg*bzYhRQBG)ZgOtxiOI-$DoREK9Lt^TbH@v*_PVa=TzOK zxiAFUo3N};a*Xe5r{J~obWuP~VD})9%8Rk8?GJ&Zgps;}8c>}pD7Yi_o~zsZ*A|QP zU{+RkgTAivmq0v&(qxH6AScYwk1R@tQhu_Zof8o!;!&<(ZF?CGlm{@`TSIPa>(u{U zSH)TdLJ#I_wf8XJL^T zOjdOkm-pV&g^1g-Hy__q> zer(#k5h!>-cnSnS7EE`~nRxdy%Y^e*)K4xs2@7!LbY->T$|iTxXW%r|Wue<+A5;p| z3M^3IS`3(r^cXYd0Wir!p1Y&Rdb_6XHx_DmdDOZTKjK__SRl^JpHMWJ-V~5_ep{4VtdzR@vcZs|O@BEU!2)^^(3}ZWG2+>AUyvkmB zlcv3xY5iGMynDqhXqcLi8eK|PGHUN~!39UuduVg}VvXGU&`fhFeN56%&#OHvi>!)3 zR9h!vt+F&y34ql-D+{{hv`z^Xj*Ns4q?$>y#)3D9vBb5$h#rh9GnU#FZD(*r8?@TM zla4P>I5&o*eci3hLkzlEq)>98r(fqig0~&F#2@$;;&xV58@@CjKDo?i?mlT+DK$ai z(%Ah>YM;lQ-*Bpb4;NaB!{)h|vEAcl*%58RR+Bg2BJ0T(D&XQY zQfvu1EzMeTX*{OAYmE>SWwotqaMohVeMLI+_^s4w4(Q@vK+YPl_M;O-go#A4az0;6 zn*5UA&+_>(Tn>N&>ZrZgeuwcxbLDq8MDoV2@zJwH4=-|7v+4PAC1#K~V1b26i^n7U z=kw@N2PEq?Nph;th<=N|sqsbT6|mz*x@Na-Oh0A&!o8sXS76)DR2}J6K!A*T?Jd~; z118Sdax}v({CL{c5A@W77nvB$d*~8hJI8;Q(Rp8WJdx6#Xg~dc*yH%k9+*NKPB-oY zv^e=coAmWnwHH1f^hRU)OOBieT`L+Czk8|Dr%SK3=ii z@nA7I8K~H=RcF3V1C*f;)gx&*g|4q$sGf-qb9grFs|hfumekIg)#4u>^7V!L?iZi^ zWV3%h$0=#ZE2V>ZTuGE0&PVA^S|q<<);|HW%-hY`mA2#F+3-a32wX|F-h)f1q;>f$ z`&q(hMivtg0U|C#Kk(t;$Y!@M64gwJY%`ri&Wg8QRMyW7OuER_?b4$}2QLX-JkWpy z2Ma5R@7oIL3Oot`0LZd)DYrVdft<|12uTp|Jft>iT_xx$> zc=*jr4%dbXWw}v9P*KWM@sz4Z{t%DJ_l6&Rx$yWge7PzpZk(a)I}FIrKeQ`Sg<`jp z(=E1T2?g=?>h)W;j?ihTLK7b9^URt6>&NrxY+|v@?DAwbGxyOiL2&UV&D(1Y8kk6ciQ{74U*==-0|pKWvY$}; z;JF+ngW<8F#oxywE_ZjXlY8?am{g7ye-3`cX%?PZ-kFMv%y;+u?1(Nh<{C`zY-C(}oCx}WWRxFjQYX%2{0W6m&#kZRn?OxZ zhlpc)6pdAU7zdpy`C=Aj8`MW%5p~kY-sdYXdl94MI|d1#cKi3d9A$O)L$uH8DN4kA zpE9aq|BDhRw_#0xTHF6S5b~Ql~?}w z;eONH_v{}><#v3et)qCnt1sYBUbAcSS0CD>uVieQ`vZj(YGvJ1lF3ay!aPH*s`+!ha$wfw`3&}z@7yq(olpXOC{d2R5MwZdHF z`+(2dh!l{s4r&OC1t8GT-rF_c610LuTuYTZ_I#=8CJAdLh*hjKQa@C-+`mPT{Tg4U zxtJ|5zOoGID;tnm>R@t_4;uX8#MGeA(z)P1I+g11{}6%`aNK{fAg&&HpGtpboi~tp z-Rzu6Vyd6=oVdk!=T-fy`W%Jv>M(4u{w98Tqih81Keqc?J5M|FU0Bb3D$)Pm&JmY9 z@ueaXcpMDzuB&yDUiFZTjuoKVtmJr};`=fncGwNe9%lq^fnIH{o!l6+$8~3~JG?u? zFCzSOWtkSB*I$>T+z&|&T4ymzuROM)kIFk|hTL<#usXBi%u$Kr^TBny;UmYz@_HeD zY^(ip^ft*~R}WF&6>>?GPMfvtbGPiad|w9iSzS`rvphNMUo}9qm(V(mcxL?4F?XO9 z%$|g3MfApJPNS~0^|{S)9)=yatFOrIyjf_&*UkQj4Xj3B-$I@B{=((byK4U8Vqp-> zw-*gB><3Y?_nI9opVtLR|ed(_`Iy7VfqOc5-k z5R4{#+54(zW513W`ddoD_42JX&7sHG!^8{I80Yzqihq?J?URRAz~M%xce$DA`o-}I z^g$-K^E{B>otl4DDVGiF!%WM^4)E}*uZONnX^jbgvIU=cvfO;AJD=_rk`eB(Ujai5 zp)rQgAEG@=@*k!Cy49=>^gR^mJMK>kIy8n(n6|6Rf4}2jzP=|k94q#K;drV;KS$qJ z#psZQuKJ$%E*)ec&!x6kb2{&69io3b8i9=+DoLpYFMj!Hq$T@Lb;@ol1_0=ddDBxo z!k(L$_%{N``XE^Fx*{sSDl@=orCJDChg$T8g`38HCC(4}x&bQlF!ns{WnwY*;xGI+ zqtL-fuC$*s%hvx~#HY)5sFG=$#UAzTbN*FB;yYvr7(P!&H|I*vrsOSq{T79#Pzm3I zmy5OdbsnFEP$Im~vylf(*PIk9T%r3l)&smS#Y~xWxh%mX6E6Yx>trzj_avE~csQz8 zl88z&9Hy0|n@?};1r45trn+C?qI5Z(q7JAgc-RfJI0pUn8n{22(o|SLj8gmys&Ysm ze*PcZ^Fs0}@PBR3OnZy{ZndsTdZ8{frK7VJYybeqMp>=MfS#!{6r@%6Svx~(Zf_R= zYunNGU$W^#C#T{4ymPCTt!;G!rwHj8O@R%2ZJzQ{@`+v*LAE4cp`2|#UzJtr*(|Bu z1TRi4uTLEL!v7d2{6V~aWsK~U%*XESU%wNuu0mB=gg~|1EBD}{Hy3UMDwW}Y`Lqw= zSG?5S{pz(*%hL~Yg#>)9*V&j7fIw$^gPu^Plx^NFT=HJKscyrubkdXitHT%vIDpt_ z+IcdyG@;%!wTz2w-8=s&a zUSkv$j6W|_wA~wxdrX)hID0t&GI!VR+z~I2-4DtbTQ*(}3X`SD`$+yCKuo~*-Hc2i<3zx2k;{+*5PFg`zGp2X=e>w|+B7VyNiY+I6~UqA@pM*k zEdwg#iSh4MO+*#({|~DBkC^@cSp0UO a$fGKDBx8iV=P=J~fV8-RSfz;3m;VC;q1s9S literal 0 HcmV?d00001 diff --git a/docs/user-guide/idea/link-ref.png b/docs/user-guide/idea/link-ref.png index a8b80f8cbfb9b4195166929d6f1811cf58acaf62..fdf82221907798e24765bbc45095867a9540f536 100644 GIT binary patch literal 26473 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 J3fBMjKLBZ+tBC*r diff --git a/docs/user-guide/idea/node-doc.png b/docs/user-guide/idea/node-doc.png index c512e26a93fa9fc3331598af3bff3b7e67f0baa8..7e534593ccf5413a686fce4a4e013c51c5b776bd 100644 GIT binary patch literal 30299 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

ZLhIvfxq)4pD}f00+`dQyioA(9sh&3eKH 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 BB$ogH literal 20572 zcmb5WbyOSylP%szkPzJ6g9Ud_aCd@xaEHM?5Hx|{4#C~sAp{6ExDD>^uJhyjR^Gn1 zyT7;Zk2!s&PoF-grmL%N)xCEjRFtGq-x9qA00321MnVk$U^-tvC`hocXBhTCp05Wu zS20-)BqXH8b>%expa5hgK52Mn94&ij>(8~Yt~nm>C+6I;U?5>&A!(?x7Hijj(2{kI zFdN_pcYqZ|MfiF7BgTYzl}xj@`jwndlbCr24@Mc0B3QgFi{aFby_dINK4DVO5Xh!r zKCBFknX&uKrgd1RklP-2-00FUF`-Ds{*n59v|)9cwQukdX_RML;S>-P)0*SK91Vk0`bM9X*R}WVb>!TaS zV=N3Xa`+^X>+1|qa&{0S<+Q;=L-k}o<{zyKb4!P*F+^42P@rzpk_J0F6E%vN;>KK!RYaKEs=@N8~j3Z$!~Ug z6&=D8P7NuJ9GpPjK;Yqt3r+*S-luA|5PVE#%o0UM+{J@OVbqR#)NWSFHPNQO#XOgS z-;x|#=fgF+mP<73gVo0rbU86%uZ+mflqYponvF%&PRn^=%v@&(!@kax)@55={oPDd z-X7cxw4Rx~S^rz0nB=(bCxNuA`QFM+&2+s~f2Nl~$SrYSi|#1%#C8Z{u4q6pSsz#{ zof99)d(qpyKpD11>o&RO(h(L&e2UO;b_%{JN;Y5t_Fl7%N;{h=u^7=%$d(fpXg)xY zxuYI>jAv%m;9UB`jm;n`(apo99o5PQ1d{DGcetcy8j-nXp~W8SoMz^_=;q9 zfm9)dz3{sN*mZCLKL<}4x!Ya5zAnK7Ql~Z~HX*Q<9&>0P?ZSJkVVCD2R7UQN$w*Lf}O_l zS~sTH-j3q_nOfe`)N9{oO(U(si=J;mth*fMnuX|*$E0TQ2E{m4!)kWQ zn7WG}0{G0CIlRxMC_=`k6+x-tW_rX5tUS6KJM`|`2+yNYd0vd^krn$I>UM-5jKLwZ+4my=+6c4fpYC_DPjKy}v0xsQui@_xRy8+sND zb(pkVaUT(|@NV%#4Z| zihe92E2FjZb<>gD#^39={`Zy$j5G^gvJCj{n#|Fi} zRiWg~Z7FZI3ZxkNf_AGnk}&Xy7s2mF0)vTdDqOAxsU9CHO0%opKN1U}ML0cUnP&_> zv>}A1i;X_fZ=+*3M=CY-_ndEU*hdHzfh$bHN*z8-zYzN@uHb}VwAKvfx)N}fQVxJp z4Nvio7cg*`f?fA5e&ncd!ID(cNmY<%jwo%dpUdR#MmBtKO2 z<-e`tS= z(?}nISfRry1=^boyV;B zA6S45^7DQ{06!KJ>t8Cr;b1S(ptt}a z^nmZL>T2ORLE{CQUtx|d!zy4L%B!4eKi83th=l_T?S3CBpT+3otu`4VA4H307Lxh% zF@CG{fAXI3v400ttIcqVMl5LNzYN57dNZ+1MT{Typdcf|068jq?nZ415AQI-;Ez`_ zZ=$%dt{3`O{l?W#NxU;;f&(r#`AZBk1D@MAqo~9iqRl?+FfzjdYc91Ds<+aDF-Z#u z*-B61Sxf74-ud@mSOsNuAQcY#qJD$MF~~|Mn$Ww^Mz}X=;@?80sDMD;SITT1W2! z3*CfmI$6W)i+Ih+o6YM0 zJ}wbnn7|oArKZ>hAI^YA7PikEFHbpnIk)w)EzYqCumIIaW>vkW^{~D=qYt%^`DZF+ zy^;zSOfpGNoSg54a&X+d{&%`UhtpSZPW7XG)(uL$fm5Tf*;wDbFu~Ikk7Kt)=-N+Y z{#^H2#zOZI(adCYV$G_K;OX@! zIQR0nihQaO)nhk^hn%VQ^e?Ks$O44o+$ifKe!B%ospbe3>FzBhvQgdISsFXK-r_xc zARG|w=FJDJ@qyP(`3QVL?^J(5cih;~w}w8Xw3XFm7psN^ zu<<312gf`GSG_kh9;NYrw65EVt~ujn+t7Bh z`B{8&jxtnz0PQe8vhE{J>=5C#KXXp3mmTqkK`v9!3(of zBl=k_P}fD+!EX@+b?lslp5$Ct4Ed&nk^sFG++@NQ53`Zb_fuhMH9_KSwr}-b$jI*0 z3aR2I(cU#FwD=##J~Y$S!4l{D_eUP+K0r#V^zZ^*IX~vZ@&~aK4yFyKr?#`@lq|0K znZ?pD#~6V3p;rMuksyLJq*2CB{LElJU4x02-?+c2#Z!BgBk#T5YpFTz*Ve)&+GrL2 zoiINpTdv&_dh_j}h%9oE+9}(ok3)dJNUbqv<`a*ENZpXzM@1AISj(c$S)^_+FxkQU zh5kmAa^#HxkGnR1iCWyxVkaDK^1U>!cP*_|Oj*4n5>`AHuch~oGxcDv?lMSX+H9dF zh$FH_Fx>{?|7^b6OxDyLzq|{N>Aqy<+dvNu0aY7 zSX#~;>)KlCptZTpD#k#b9|0%5U-n;2fQ{{0lCVmmvbMHnv(af1BvwA}*v0>1!1BEA zEQt0?0!fj4l`sQDEVCP0b;(K2^m;XtBJ1i&wK68AL|^#xmmwc=B--r;I>h&^K^S69 zrk^F~3Ov{L>zO$k%Nn1*vA$CmM1MuIypHafhZ0CRj(z0^=Ttp}e&>VU2S;#bRHQ|(L?zO z**3!H9jnkwK0>g#&wB``hpmqZVs*Xp(|%qBS`;pTbh>Emrx>e}S}*ec?@Co?eobq* z3WMr5V3+Xu9aR*{4@@_YWqr_OQh$|Anod$j1)^WDGI`!5QHUs=;Cz>)`jh?WN;&JOa>5G))GuZ}y!i3PH`Aw*KmlIA=)7a<*;=|uR6>-$^HH&F%# zmfo$1T8;a4dujb5!DoFgWakPRo{G`MNzBWji5l5J>qoRr&Fjm4#COm#+Q4o(n5v0lrWt_b-nQ)907I);M z_1mIbKu6~~@)NgRm$?f4R&}cK&L05T4sWu07#KXnhr2(;EJ0HhV1VV7hY_F3Odsqy z$xEHO8+7Q56BrH9@RYiWC+@AK{^na$*IByzEH11y5`T)z%Cmk=cPGsfb*fhd?>3OF zq>e61k|?Gl>fdqD!LySTJ;!=wlr67KM4xaL*xlY6@CE>4UcMPkXE-b0bUrdeE{|@m zfR>-qNJ2Lo!!a;`C*$h#jNY~b#mjK^y?N($Fx0oph3~U$_mE{ks^jzBH;EhfhxD7Aqu_Ecgkp6vCVua0hO z;h|d5Ei%b%2b{KsgM4m>>6Qp>bKOVsz^9HCs6U!{|J?N93c4~4$hXZfKNqnQ96d&tPnomRTs7#UKl<6%H z*bA9DAaKR|nx|Ot+`$Y7F;v2&E@%S@Z0=!T()ZrEH{=wpj4y77jIV{jn_AU)0o|K9 zzrRv#u_(5=ve`y(P?&U1tIgzbJI5zvGEzmqdz&ceLtJ6D3MHOa7~BjNr`}?Ws4J?i zD(gE?y&uRQvbqlJLj@=zV|1z|)~#o_v>l*vS5FZ#6{ItNf-f55AqZUz_CNO}uPu0S>eT|DLlp=>qLH7EpQG;+) zsz~Rw5M~d?)#&h}QTkwo$~~s7a+IG2;@ZC?VWU7$90Si5olvLtw0GyR^y%SrwCZB` ze-AH1iB82L{Gf}>(h30JW-(YB@WX&$?D2Ouu-mF)T@&@MI+D`MoS4POIzPhFlsQj4CFf}YemPskG&!Kf&T_pt1sI7U z0)?f9j2Fr5KKO3X+HUSaLl|JU<-bF&4s27|HPex~l>`j=Qc}F(At4-q!dkw)&ncF>DP`<=;&lv5M`PT^UX z>4)(0!OvQia;eyEnzwUItu=kyXE&^QC!AT={K$_HPlC7yDG(`Qwzbo z?h|Ej{Eh7!e$<=n8%BN0bOFKc9vv~99-=EDN2C&G5Rqc&OsN`}T%Rnx-<@sp>ag16 zBE^)V(+a5GSU4%}xFa^%t=QbPCUaX21Unj{b+x>;0 zj2!*+g*(GR!Pmhg(8xKPfz3&>zG&^|#94GQ&+Jo20+apLEUfiIg9Se;1S z!V88l0|LS%fD_5C6FbTG2ay%Xxe|42N3nLBkfrK$YLy@@1L1k}L6ff3t$54Ct{-2~ z)SCqD55TW`wUVE2^{;#JwSH7ayMpA90F}uQAFMpwO})(7$imtKy>=1BFP9q}9B&F+ zS6a!6hDR|aKMuL<&+rag&)$U6_0hvuz>%dIsQ?vS931aWYHO#Ow1ABCoB{t%@em;q z7>ID`*bp08(xpSul9GXWSfwFJ8fkn69_ifNc>ei&WSy9)_3Hr+Md^?Ug%y%v=@rzcTK&=nHfUVP!B)eKv=u zO}%!kU;4xS(c#GxMbO?nLJdsK*8UCY>C2yr)Q!XkRa_UdboM6C_Nnk==9Z;Shtb?S zGlZ`1@Os;?6&bNnCLaqJI77!mW_uro2ne~ig-z)DKK5<|sY?H;x$#E;92$E4{Uito zs7)Z#4ckdQRqu29QD<{ek7xO#o?-t;sVAtdvo-9`GbCqrcC?D14?A_^UjR~ASsSWb z7A!VxnSR?3KbvJ*BGHMeSlpbNT#qg*Uh`v@Ey;mF9opE=#KP%v+Mj}|ScuL|QoVCw z@t#$|>VU_}@qn{#`XDm3g=uno=#7h?m+kSf3+u5={>ZLk%~=~{2*7-F;pf{Ym4b>QZ{x4eCd==4uk;J^>B+{?ylK$PRo|Y^hZnZ} zc(&6}$(O*Oc6PHG`HpWMmPLnkW)(9KrJrdtitQx$0WbV*GRom2;wro0PX(o(hUDL>`(P+smMRxLWo&{nX&1>vP1U4jP7S#lKBHvFHB5E*2% zi+9@lJvj;?1DEQNrqA~wPB|u%#^4!8^HBc@?Z- za$nQZ=ho}ZZ6VYOv|7rGRLx2_8{JoSvHAY zHm~k)yst>aW`L!}$}{LQ!T~AoHv53hXQ$YaP1xZC@`B@}-bnSG5fY@|l0{Xf%cl}k zp1U5(Nx4ZBso?=%g7$AE7`aF#Eq|DWDX98l8qx0z(}bM8sr|@qf7?v@u2->TCnDzW zgq~xBHVKa-CJ4l*6Cvah)U^jGQf$|f;1+!!GOqx~k{k>Z!Nn!4eZn>Ky~Q!HX3v0) zm7cwCD$kOhJ!YRb{a9R@<|k4A^_h=#IL7LxLul(`Fq?lW& zhHugBVmXH9w$cci84!DJWxHE4XFHt-$*v5Q*V;S6ZQR_R%O_j(-KtA!u88p!6$7V0 zm_+V(gND`xoA$FftDd`puG2qP26#4f5oB2SdB|_`8j?(mnmi5*Lf|n~Bo$Ix#v_1X5(RlKlKYproIcdPr3rt_nWvqMvbvXPIWCBY@} zuuhpEQOJ6YHTz{>h^v4=S53+>$(iOQSvWFTD+9SH@5qlgVAVx6eGBp<=Wi9Mya%4a zlZ^=vRLde;;JiNi1pp*WKf8e}{&BSAWv?4xM3NHR0JL;YR3;NkJPHZZp++rU0YKyd= z{A;6#@Fm0TcxYSIqjKFph##kxr;ZRQXQ8ODaOZQR8A$23 zRlj=8E2qg=@PudEjVAbiMRO95Gc8xB__|l|2(5l)sPDsHs7D+#z&nGou#DMDE@iJ6%Y_0vhAGS_8`cJDusv$ z(6Eu@xrX!6?DdeI-}X)IM$ZU&_2hRcq8L$!_~I*Lwee6&nI2Xi*>ST=P<>_~96Qq( z__|j-B2>^;(v-6_7+zp<*~>AD%ot;>KnB>k!2-1Ec7K&UeQS!ZeHUX^=1LqNA;v-? z*#-N0sS4>E=STx+%rSEGWNz0*w=?Ay-s2E1|skyJ-pWffFhW#@Tfxv)aQbP&C; z*|u8L?{?H1XYX*k+REBJyHNpS&eU*_yo_B&XYC*yK_)=s!Ltm7 z=mH1R#Ml0|`)qRdYcX!jkqmFP*XgkCVhUa4?yRt|BY%Y7OVcZMmqC z=C}@ny8oI*?M|;d4qL=ZLx1TKAH}xFW3%2=Plp>8_1{g;IwXB}Zq)2LrY1S?tqSck zntpt?1O+-0O3e3Yc?GmO!})+(OYqGAz7P0BP|d8TT(S;1_KmzJTGS`e30_J1>q|J- zVDm2vIAA-T6tO%p8jbj{-aYuW1N6^Ai=+*_qMOU+g}rnFBFr4b4vkGg< zO>**5&Kdw9GcXcuqGu46)zNYuwNWi^+`DA0N|CAplXYGN`KX23qR4@jG%zXr*QtIz z?QU%`S_6#qKVcc!tj{+9yyl>!3j|@|N<)g$4C$<4LVhvVzD`~+2o35To|o*Xhe?g8 zO|PTjEky$Wlw^MLW-*y>F#c3LuJ{q^?}qmLn-;(YHe+ghc3zw&t~dp$WSW56BgnHD zp&v3c|I2M<%O~XL*cyu0TBlNs%Exo0Ly~2WO@bI=dde7BV>W@;QS!fAe%oSMtAQNP z&o@#KUFPhTFix|1<{ZBSE$(rzLeGf+!0B_q`51LYa5f@toDX!WWo~wo_b$NS>fi#G z8dd##iFSB5T8NI%+=~rhW1byC&W(3<*Gy_eljhz%AU2eUBmdd@kafLJDF&#}FzaP= zsr@E-+Se;uv_1nFLZBv^M;BqAT?)dKr}=0G7tJIkFn9tq=l{MmWZT(K!}5HZ$c#R( zrdT^iRybQcvapKA3ikqfS6P2!hX- zC@#up+v#0`02Ke7iHSL=rnB!d3G=HiE^OSom`4*-{lje3Wuf{t@@kIs{@{xnbd5EH z%Xw4$wlGSU70=_HNVm3IMo7E8Wp)td=iF;enwz%*!@y`3! zOJ|C=z)JjQ!NWl#y=|D zqyT^N22)J|J-3Vb`}cXqCrKnj6QKK}f}IHzu$l=|=Vc!5-1JA=fth)u$I%|LWD7Qk zw&H%Q`1R}xdkcL^+on+?P0RyZmHT{ZBXt0@7J{6yys@VGJYy+eAGGhK12+)WYH)Fv zcxxxGrE7yuN-hv9s}V)^bW(~$26Xq6zj*#g-@!I8v1jE(1Okf^oI-b6-!r|ZOG6dP z^j7rt)iC^^LHIEGffjEFHoT;C^*5|j+a1kcKqB^f z4f`ok#SlRANTyME~ucOqnD6q0RKJ$8Ne7?nA2F93bj|_ zlJ_jHpo1)-i2cUvmqJE9yx{{qtk>zS?VnT%9Y!moXQfm8vTAB5s%jN;BtO`D-(- zo>mH)fmM?;pdWCYn%4na!ua!)pt@PxYvW=YIji+jlC(Fug1^;h47S<3^sNs7NLcrz zMwr^Wt@rUD0D-&%thT-&ES*2?<>eEpQYB)N)S_qRJ*qYA&gZUbI}-E=qV7~^V=FHp z9ssC@pq%<^Tdf7stKpBtVy+$O&kt1$>;G6+Yt=F{riRgu(zVx)i^rfVsq8q;Gs0}m zGX^Q4X9;l#^Lw0sg~uf*MJF#;D~Dt?ZLJ_C4s`M9T7I$XKyC1YX+d|?eT%azi=d3b9!VxJNcAeo+ z6uI4UZ9M+*JXEv>E&aX1<sQJ}6pgYSN3{u2KvD zAf%2f^94cz)Cc^UcW`=}hsW}L4Gy=%S3No>1=d(VspDR@Z!5VnlSNg*J1x#V4Z>8~ zhBy^d%rU#%B&T^hlN`Dfrq!(UyopqIKB)Lf^HJ0)tIkCIJ$60x*;SYfJUeFs$9NDr zGet@}&}jjR?Dh{*IwcctA3qOQawJ2{TdxB zSk9n^={hY(I;*g^>XyTm*KS0EKWz+m>hdy46i2cS$NY!h&tZ@1*C15Zj!Mc7(tb^W zZ!&z23taQaDTk(K_%VMmf3PJ+QrIm17$B= z`KdIeyj@~tT+;)rc4K|Nx3r}lYVp34&R~NM(Z^$AP>p$b@aS{;*MjbuiRq|;V*(ioKr20xmv7FC8O-nQ0bP2g2DXIoy22=YpL>-2H|J<9{X-Tuz-BR2R8CY@ciVTg8jaCWfR~ zCdeW1Eh48(qj9RBG2NgAi!#I^0$rMJeM|=@Yct>&VdZNEMHkvJA+wTCC`KA#rx*46 z|Ba#Rcf@(D8e7v0s5m4}&v=oYPG`l!lSapyay54~h-*#R;=5UoY^d+zwh`d=EB(16*A_+&|>oQOMci&9*>$TnSB{_(&e)M-zk1^MF>>>O=fa#R@6 zcWUj-br7*m8}Fs%A*>ngW^x*c>ne+fftMvt4fq(+a+%o3XrF2;Xa1@_)9su(*!r)~ zaOHkQ7z>i3$6kOzz$n7(%R*TJDU(@Oo%_mRdZy&=qLFB!J+Yw?mepkq)noKh^;UcZ zDi(Ab8PsDdwTmbhf0@5bsDn_Yi3i$&%zNI;Lc6z!>Bb~>*2r7CpAUpgrM7e#R(AsCMN0HZ{=?biVpdXj zh9I;JD>unU6fLBf@Ac0zYVI=Ml^#t8;441)tBs6tw#0eMCnw?-YqW7lMzh%7?j_Og zGSE+pUaKsT1wAe?;X%r3ktn&6s-$))qT5=(q8YWY$S>&@+h@{GYnP=*hkJhz|>%g)m^Pi@)?K6Fx07M0}C zw0TD@)7$P&spK`|eryN^#}{UT9PR`}FKe&_Z(e1FO?zN#w-qj`@JP(Z!Gv2847nj& z<&8j(ka6ZVl-zhVp)(K!k8n|Q@#Qx@0&&)#SJi248-+mXOEUEMX_&JE|8=O|V)>^m zq6y`HAOrp{#^`@j4Y!sdi^G|j_N`QYi+U9ix29w2=vyh2Tj5Fm>uWDM)fR-2)`w*D zVb?1?eXT4W{J)Lc2!Lk|a+PS0sEdy8syA)1;0q|t`)<7$XXxSlQ1i3_m06!525qs8 z)DNRQX(YdAvCbgIjpy`Jwe<6x)8K z3O(|ta{OJ90}^_|r56tHXXp5pXz$m!wQh5FT9(e?5~AkgQhT3#i6p-0g9wp-yqe9? zIR7u|P4B8UBZZx%#IQWMK$bt(O;c>r*zL?*rR+k(Bs89<1LY$I?q&8^pCrh=>)zd! zPG-(Q@$Q&&TG`O1tZkyTH#l5qv&WJ=-Llp5F0YE4Mdv8)^_76ab#+1SjmvBWV$^Zc z({XV(AZIFAuh=1pae3+PW9(7SUe6Y&$$-zZ#qxk*2t`?0O9!1lJm97hF)1{v4`=g= ztsDET+Z)FdCT^FH-;!5IqI=2ih2B(NfY3mgWDVdPdiz^N($#2uqF*9r^qghqSG*@=8bwc+f_{ zAz6K_wvh*wDqcQ`>dBD+#-$VISlKat6djiy=2tY#%8a1>{tv3l1CIWxrt843iiQ~c zr>P&rb>Qc`aV~=}xh>bZO0>#{yW`FgX^vJ=k#P!Lr_<+~DaX^Pk>v*NUw84F_J&gN zeEudIAMXdANe~Mjp*r!6n}R0hj#&q4Usk)+A@(}}FuxLY+UXq|srxKZ`I0wQ7Dimu zzE!cRSnhKv^C)AWFwla&D#ZH~&*rv^J%M{l)}>k&|5m5QC&C#TWJ8CW)Z|n15?^(& z1azxQj2gu?aaEok@+x~6=i%NHB{kkoeYB!QU31rY7k#dX+rOeN@FYq3A1O+s$ban+ zcjkyTFHR7<_QsAr0q$pj#NO3ew_*x)4OG`%1P>pmW9sl7xDK9z3L3Lt73knPju(!98c zuk$wGg_iLdH3)rF3K~v#5X9vZ9qw9BmwM0R=xW@1?ouSP=H9}O5h~5drTL($srrdM z_6GcR;QA02Sd-jd&c^4RKcO)Vh+sR5T^82?6Ag8V7}|H9tWv`US7^!}O+L9Xv^zA| zU$~2kbw->$tNnhJXyd+pQK_y*6Uhdg*5oG!XUNMv8wG6~{rlU76~dH3zPev{zB`mU zl~47;00Yiw_`N-AFRvznz$%QmGj0^LDnj+VhD_1>1)~~mg+2fDLmL7o_w*mwGBjWH z1hej%n-z)Y;JRxz=@zFCCer6$n8K>cm+Ee7dO$;Rc!P;TmTF6;uVSDC=A?N;(b6m% z!5{Q$7n3A2(iHDKoO8?0IN;=Rx{pE5PiHB{enl43t7we${DY#}4{PYPFi1v!czjs_@{i}tGb2!PZ?-Jk zGaZGUOP}+iq?X4$vdyG7egw0RlKn?5qjUdGvvWK)(7oVP{I)EP78k!m3LS!Hhu^UB7qvDZ&RUMB8?q27z%hERw5t?un$BHi zW#)Z#Jy}kuqyT^f?frqpi%bF{AP@;x!-pHdFy8^6Rzz$q7vp_)eOX42yn$9|q}KSg z6>QnPj(#Ejj3rcdhD$Bfk1LFL;K2YPMO$N-;_h$qrar@OnMnW)$uIB)Eyg?j(saB| z+hrb=uMQasKF)vv*ntK4A%%%;^v?ZP(w~MRkO0+P|1-`g9?1X)ASOa?d^AWKxdC(h zD{RaIQzyans@ZEb3jB$rqS7j0{q1@zgReaQ{|_e@-(Pt$S+hZ_eBx<}50yymr;XBS z_cCkFIm<3kCtUyu4@75^>{lySr;=mj9PnR*_t7kxXYN;w%PtvR7wA|M6}JoSDEZTq z2ECcYlU8|Gb+62$y7X74NfS~J<}}v7%SVs%WVrztI!=K#^}1Qm`I|=SftiWHi|DA< z!%S7$jq&?3UKW%Cf%E$X3lFg^YMsH_EN@2t(P-{ZZbrwfGcnLMj>H&bN~1s8S1c*` z2`L-<6PyVt16S#z?!bRRzBNN)uK%e?9hvToJ2?UG8o8XTU^VzCYlOqwSy;-)J3td= z^>^*uvO6Ajx@WI#$p%%Cm*6tp23Gk%JG0WO^(8ZNH&fCoL6kig`9FN92?^#7-~nUE zSisc}??}scG_r@MdPH0MJm({dyTbzYUt9Ra?C-tA!C-S&)X<~qo3Getz}kmv=@{6v z={G@(r;BZYdFh>dxiI~_HW7UE{I-L&Zzeb*SG_j!EKpF!KkGMH`MeZrNLJ@<4Ud&c zXMZjHjdu!3J7K+zcZw0MNk9l1Q9kmz@{tz+`+;&XrP*=>0n3ULD~!;A!oCrFZ1Dcx zQ@?4WD_|GkC7n(|Wn=C8ZnwsrFij~vw-G)Xhx%1L3s58vSA0GkIqry&lb7dAE4sE{ zFHz$?(k6Ld29uHaJpWo40ih{D5Nwy<8Y8esT-=-FP}?|1gjZ$&97I8&?8Ug%_bgj7mRI>4Vo<(Zx?QP&L$~$R>ds4`Ec;O_px`}2qt&L z`;Jl*22Dv!M5-WRZhCr24pqdLdZ(ODg|=e8#Wv`9X|CX*%>+z5|jn>WPQZE8i6t!pUY)Wx||9fsujUEnpst3GtReH zc%*zzIZC@*01!UT%E9e1+W$_?<*Zo1Zi-qDPhyBpLxw@AEg(pSp)(@j zQcO$?_(+e_y37ab*#7e;Mv*Pi3LaIALH$Ek#W2-qL0ELcO*Q4^WPyQJ>sFMHi^TRu5ib~%ie-; zLXt-tii;Od>0K|a3cL|gxRs^_@EQ)SA>Bv(!_XIUEdsr=-TC{G4Hc>{2 zTO5Mz4^5Sg_<~2g9QLH{&PyBmP;gu%nIIyt@uz|KsT4iYS|@U#C@`nUGt>hq=kIto zGozFLi>aDgg??4BszK6GW2bK@B+OkR%X-B+UxCouME$Qggv{ZHc|>W@Q`{fe?UYzqfVi@=(HSaM(}LAyPl|D#Wl zU;P!#f(=kKsV|ZLayA|t5_wN$BO+@zo7?AJ6?%l>cU<(e&)Z73p{TnHHBi2Uy8fg9 z;a?p4%v-YS8^h%Y621zsn^ExQe{D@H47;f11#4o>E*bc&gG}o1d6TKO4%q}OyA7(l z5yHQUf6K)0;>&Bm(j0;u>+xu>Jd{s<_4tyz0GsF9y0na9SD(G;QEE)@@JK92?Baod zsAAl+$U9*dG{JA+6!Zhrw?NEp(ky^<^nDXvrqG0$&sR?32Y>g4;A7;|C1Di1v*0>c zYC(DM>OuV@(tVs>q|&EmQ0i~jdQ4YW*yiq6qhsv*@SbK-9s?eL|QI(AL zwtC%_L9S!mYwaNPw6gbxC#58t0Av#>8nC%_21^_<0a8#g(o+r?Zh5#uJoz!1@tp4m zLf|ay=PaToU*6HuSQ&r?EDy?0N!+&6tK?FMzqDWN6aJ$(t#d%))Mp~lEv`sje)Y5Q zj4!MVXLqU!O~4XM5H;C65}99Xg_R*zYkjrIJd`SupmR+O2jMO)LsgrHvq<$_6eEiuM(gv3#0kihQL@D{Hrvyl zB6Bq+w}+P@q@6%EK>f#xsNAZP{}C*4VHxK#I&1a$dGQ;bhc%nSsVP0ss}1=q4h8{# zUM3@*N8f8{{D7AOPvszXX2q4H+TK_qZIGoxnlxG2)W)3NLRG}5F z_usa;5Z9``)5^&*LA%R!=Z%SVsFKgZH3 z{>57>Tgq}^#Nd%$HFZbnB5R0~KK`BxPgiZ-JOs5eT7iFYbgEkh5(I`ZHd57ZjYL{Bv(Upfw-_2$rD}p`E zJcuhz?A?Wjk2ZziNf)klep@tuw)$>_G4@|go`DmF4^JIfRrvExv*i924xT|j*aG}i zBdFaZTVIw2l6AjxUHNbP)y+&KnJ%3}QrBWdpYy-$xyGBfV%j6VZ|2{aox}TKU>xiU zN+$8{djDGC18+@o+m_)c7C<$p$=-d>{5@6qElNqcXtT=Wdgxiyif)4btL&d@UATB# zKbd;K8Ob2S^h?kU>~|CRu{i5Stz5rDgPvoCphMOS}~#m5c&&Uz0b+zZ$L5~FUC znR!P=Un$+0qIH0ImGG&#;@=C*4c1hk7UB~#JGp2rTG&o2BPV+!0`P;vAEZsW$psd`Gnp-d+x+G3 z&1o0wlRu!Sv+9b!6ufV(ivF~N(LpDD-m8(--CJKe3e~i#bpA&Jn(Z0?6d4wm_+t9I zIfbWTzYq5D@1O!Jg6u2W!0`;Ae!uB`m|00|M~5?~W^yOl$4xABBiio0nJ&kd33*zz zshi`5t43Hfsv@nA!4R8*!=F!Kcx}jH9&8yPhtM@xVx;B4SDf(sA2=aanpL-zp8LHF z!;aVY%)~dAtYsCRV8T(NCSHMsDa2N#)mfsVt{p-r-VfC<vx6aDrfDdVmKWFricnap*|oD+RV6Jy zyxnj11p%(uqhywb#cj^6bQeIky7*-EiqT1gYJT)R|Wt<2hM9l3{-T!U3~edoZKb zDWYcIcPL>s+r5{8?9l^D3|=XnLM*iyRnZUW?|D7<&!3VnE-8s;>T#{@A0(DVI~+pJ z!oQCOe6CpkN!xYh%ty^3)`?VA{m7GnNl2L;GO^!VC`8SH+kQ3CA^(-mE~@q+>NG%w z&9kZXbTs_`3$F}N^9%qyMb!Kyc}wH^zJBv`2i|Xl?hqKAzH90;BD4CU4_+yocz!yL zr9Fes^1&IAC9zBM*QM~f)i(F_o{Er9!swh~N~I!y)5h?yFimgo(PPKWX3NMXFJ?sM z7>|3B2AfaLh}= zbKKUuX+~s(?j#tUGt6wZeDdjM*V{VlXGEUz?jz(|FgktSWHTc3V%L3t*L>LxF12uu zs?T3L+U|=pA|rH%!szs2Q=1W)={o=R-wSV85l6ciamr>NoDq3w|IB3{y>@Hw6++)G zj7}dm#Tk(qx7|L45#=}|GD5cxPIY`EIMp#ivl32qj1WRI0HYHjguI5)i4a0w!{|f^ zA+KR{B7~6FFgkr4F0g@+FT!!1z6}@HK*%Rybo#KnV1W%v9-X}`Cp9`)C1K6owHHno zHk)p8Vkm^ZO&FcN>n>Pe14O~0!p@6doY7i|DrN2VEgLLveO76~``eMv!|0r0clQDt z?dSHLDj15}{)WJ?gY~kodL-a`Et{~GTTjbyl$oJod_4$001D;gf;mox^MP1O>%w}As;av z(MjU7WA{p%|9^Y;8r0O8#sT~}=j2WxTtYBlAc!DUsUozBH#A}eDW=+1*R88=opGGD zyX{On)60J7cDK`Rr`k?;wmY@kcAV-4|JoCh9xTYKPrD%!54$qY_wk~(fqyC<^mujQLjEf zFC&5{q=0~hh5rBm0JrJQmmAIx0{{@B;G9j0O(~Lu2rg%MSleniny)SEV_*ht0YI#n zvukrqW`Zb`bZZ-RpXPU5x-l?Di$V{)82fmo_V5`)lRe<_b4s(iz=kk6OPwA?xI%aC zUkw1jxLdwIGUe(}gjnI0TOA8z((UT_ZSkJsv)4Z_wT_Nj5u(UHy`G%Zq&ZwVV5X@E zS+Lx0I9can0D$5uq6I>dxBfSawhn4vD(D{u0LG*18XCD?;RdVoq-4iHyV>m)h?cA3 zw~4x+J=me4r*F%h8+-5tWprovXXi|vAX4Sd%cgo>d{5JO|8<^H#@W3T&J$ZZvUyGwh}G!@dF=mhuzdUhQXq;)(cEe*`if*ASWX=Vu+^%iRBgd|OG=(f);Y1jMtRq`9snn4%l`RIhYMSL{|}s~)jM~j zik!qqfsOj+UMC@pNmHpZ63FodHV|TsJKySflD8Zg6udEe-rnW>>m`~Ojv2K#58M;M z0y*E?P^Bwt^#TC2jp@dF-JP9oAoz=23^O>?+F=|2zH!?Mz8+b(NRYPXLD8z`!&5^$ zH0J5r65PzqiU%CJ*FJ8$Gz0+F1|ya5THKS1JO6sc3wj7}mcEBVDt&Xw;P-YpuiOc+a}jeSL$*!{bf(C6B}DmS+e z{gQAZc@M5r?p_&@9LW_>1OWhEyNFLrUmlSPS(ZTcR`%Ap2+FsX&|jv|2>JS@h3jv6*xt?=JH*G22+P?J`{ctxw))ODj`x%W zc5Whs*lV&eGTPmGN%Q)Z&V;3NpUO@-l5IctL;Dxy7USsEo!eRULvz&y&7y~552%w5 zNnzh-z5Ub16e-L|6+0>$-Y78pu16@+Gfk6G!ycQP(Udm4Q{fu>JYL6;?+|=xxH>!T ziBy5}@aXZI{oJB0Jy89Vcg-B0wYzV! zOGzSeRop7Zbm(tAl|~FrgwQ=l$z-xAe}T5>t;OZN*BlJP003|jV_#8)2uYGJ;zcW? zo?Iu5=+Yg%>=@H!ru==;e~SYz$J+p|J4nat^$ z!3+#SD4+=VJfqQgCr6kzI!7?0p~M%iSP{KxW%SlexoUyH(cJm|$)0gbP|RA3E=ZEO zdhQdMbJiva&Gm*_D*!M(8k;*NQk|v9TPBIH8bAEPEXxQnm5+_iTeStRZJ@YrNHRYv zZ~feDYoeAXh=g`qRlTL#dGqPrRT!NJ%}RG3owr=qiNNpcIspKPu(Zci^!cx=M^^uf zQy$$SuKM>y!|mP3PK2lCEFJh?QQK|a@<=JxKCW!fFKHjQXq}g=-lM4f>>Ms;M*-oU ztm{N9f(14ZVkL}D7P}7$Y!G5adPe#KvC~%jB*Z|sX zZYe%_-aP2xNan3n=VaNBep~0?F%pDk0Hf2-u)s#^$d#8jD=vUqHcI#~=0Y#_u|Bin!A4Ev;5u>A)@=w}j%BsVt~08XDiZM9l&`#5*L z{l}!YJtKq=8!44ai9{liNR&#Yf6pXPI_%_%T^Vn#7^y_$)ptDimZs(IE@0t>?tf`f zmZ|NJ|I+Jmo&q6O(rUH7jKT2%+uX1e?qJ{-4V}q`fX&jg1hCB_<{TKx=D@ ze@}zO(MfRTr$vUf>8kpFr6*)OUYrs}h|Llcld`fl_`ddUda^S*329>V-mP(K_@;v& z=w{Z0l&2{6Ju)XPhR1O_o9cRwo$jd_-7k`$5?3ZXvoUf(C}Gp}oya#v?^z%|aq+EM z8h}u2O#MV1?0X?R*W32K-=)J2J3?qmtcy;8OuAhizb)QVe70dDS>O!tMl z-8H-r5idi%_^I*tY9oYLWb{fCJM2FrgMv3^&)d74f4xNW!ZD+Enmx*IYMGlO6E(Cv zchX>H0Ki(OCu3fT-Id<`N|}oxgR(bB`PW+B{8n#f0NSk0F8+~y5u>f?VYi{zZGfRU n0}O=SgFg%*bocl@G=2ULkGR9u@!py900000NkvXXu0mjfWEpqO -- Gitee