diff --git a/docs/user-guide/idea/attr-doc.png b/docs/user-guide/idea/attr-doc.png index 8a70910de642a9f8a2db734b8d1668c8684a0aeb..d4be818d94af006dabe13af8ad621bdfad87411e 100644 Binary files a/docs/user-guide/idea/attr-doc.png and b/docs/user-guide/idea/attr-doc.png differ 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 Binary files /dev/null and b/docs/user-guide/idea/idea-check-1.jpg differ diff --git a/docs/user-guide/idea/idea-check.jpg b/docs/user-guide/idea/idea-check.jpg index 57b4c697ea911a28e5d3626718c88629da60f567..885bd601feefded37562a6d1d584abf3516416fd 100644 Binary files a/docs/user-guide/idea/idea-check.jpg and b/docs/user-guide/idea/idea-check.jpg differ diff --git a/docs/user-guide/idea/idea-completion-1.jpg b/docs/user-guide/idea/idea-completion-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b2431ef7c8abce2c96252152bb2ccf8170bfac1 Binary files /dev/null and b/docs/user-guide/idea/idea-completion-1.jpg differ diff --git a/docs/user-guide/idea/idea-completion-2.jpg b/docs/user-guide/idea/idea-completion-2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8621600df7bb08c36703d0f939b481b2864de95 Binary files /dev/null and b/docs/user-guide/idea/idea-completion-2.jpg differ diff --git a/docs/user-guide/idea/idea-completion.jpg b/docs/user-guide/idea/idea-completion.jpg index dd0a6f03e7e9e7981ce84b6ccfb3480a2bdd4fe3..51119eb8a459fb363804bf6e2f49172da06ba7cb 100644 Binary files a/docs/user-guide/idea/idea-completion.jpg and b/docs/user-guide/idea/idea-completion.jpg differ 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 Binary files /dev/null and b/docs/user-guide/idea/idea-link-1.png differ diff --git a/docs/user-guide/idea/idea-link.png b/docs/user-guide/idea/idea-link.png index d06e1a9534d475b1ca0ef1038ba9cf50256202c3..abcede568cc8d135e07192d9e281e0b9fbeab719 100644 Binary files a/docs/user-guide/idea/idea-link.png and b/docs/user-guide/idea/idea-link.png differ diff --git a/docs/user-guide/idea/idea-plugin.md b/docs/user-guide/idea/idea-plugin.md index b90bd749682b108e198796fcdda9f62be68d6c8d..84d0eb4238a7073b8134380cf73810a96d776f13 100644 --- a/docs/user-guide/idea/idea-plugin.md +++ b/docs/user-guide/idea/idea-plugin.md @@ -1,48 +1,75 @@ # XLang DSL Plugin -在Nop平台中,所有的DSL都采用XML语法格式,使用统一的xdef元模型来提供规范化的形式约束和基本的属性语义。基于xdef元模型,我们可以实现统一的语法提示、关联分析、断点调试等功能,而无需针对每个DSL语言单独编写IDE插件。 +在 Nop 平台中,所有的 DSL 都采用 XML 语法格式,使用统一的 xdef 元模型来提供规范化的形式约束和基本的属性语义。基于 xdef 元模型,我们可以实现统一的语法提示、关联分析、断点调试等功能,而无需针对每个 DSL 语言单独编写 IDE 插件。 > 插件的编译、安装可以参考文档: [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 Binary files /dev/null and b/docs/user-guide/idea/idea-quick-doc-1.jpg differ diff --git a/docs/user-guide/idea/idea-quick-doc.jpg b/docs/user-guide/idea/idea-quick-doc.jpg index 1fcdafbea127aaf8cfdba20f24668542722fbfe6..ebdaf64ba03378c26b3bc440a161c19ce22b3569 100644 Binary files a/docs/user-guide/idea/idea-quick-doc.jpg and b/docs/user-guide/idea/idea-quick-doc.jpg differ 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 Binary files /dev/null and b/docs/user-guide/idea/idea-xscript-1.png differ 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 Binary files /dev/null and b/docs/user-guide/idea/idea-xscript-2.png differ 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 Binary files /dev/null and b/docs/user-guide/idea/idea-xscript-3.png differ 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 Binary files /dev/null and b/docs/user-guide/idea/idea-xscript.png differ diff --git a/docs/user-guide/idea/link-ref.png b/docs/user-guide/idea/link-ref.png index a8b80f8cbfb9b4195166929d6f1811cf58acaf62..fdf82221907798e24765bbc45095867a9540f536 100644 Binary files a/docs/user-guide/idea/link-ref.png and b/docs/user-guide/idea/link-ref.png differ diff --git a/docs/user-guide/idea/node-doc.png b/docs/user-guide/idea/node-doc.png index c512e26a93fa9fc3331598af3bff3b7e67f0baa8..7e534593ccf5413a686fce4a4e013c51c5b776bd 100644 Binary files a/docs/user-guide/idea/node-doc.png and b/docs/user-guide/idea/node-doc.png differ diff --git a/nop-idea-plugin/README.md b/nop-idea-plugin/README.md index a28fea251bbea1b7239d8bea709df69274cccae6..f9d42bbb18f3f5486f8b57c0b7484d7a3ee8ecd6 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 实例中操作, +便可以像调试普通代码一样对当前插件进行调试。 diff --git a/nop-idea-plugin/build.gradle.kts b/nop-idea-plugin/build.gradle.kts index 0fc06615f13bdfcc4aa8e3bdde4335b49b6c9bf0..27bb061e996440ac02b8a90368f8b8c52a8abf40 100644 --- a/nop-idea-plugin/build.gradle.kts +++ b/nop-idea-plugin/build.gradle.kts @@ -1,4 +1,3 @@ - plugins { id("java") id("org.jetbrains.kotlin.jvm") version "1.9.25" @@ -23,31 +22,35 @@ intellij { //version.set("2022.3") type.set("IC") // Target IDE Platform - plugins.set(listOf("java")) + plugins.set(listOf("java", "org.jetbrains.plugins.yaml")) } dependencies { + // ANTLR 适配器:https://github.com/antlr/antlr4-intellij-adaptor + implementation("org.antlr:antlr4-intellij-adaptor:0.1") + implementation("io.github.entropy-cloud:nop-markdown:2.0.0-SNAPSHOT") implementation("io.github.entropy-cloud:nop-xlang-debugger:2.0.0-SNAPSHOT") implementation("io.github.entropy-cloud:nop-xlang:2.0.0-SNAPSHOT") { //exclude antlr4's dependency icu4j since it is not necessary and is too large. exclude(group = "com.ibm.icu") } + testImplementation("junit:junit:4.13.2") } tasks { - compileJava{ + compileJava { options.encoding = "UTF-8" } - compileTestJava{ + compileTestJava { options.encoding = "UTF-8" } - javadoc{ + javadoc { options.encoding = "UTF-8" } 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 deleted file mode 100644 index 66f32600c6ef7c7696949d03303b92720ed32980..0000000000000000000000000000000000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotation/XLangAnnotator.java +++ /dev/null @@ -1,288 +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.annotation; - -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.psi.PsiElement; -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.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.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.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; - -public class XLangAnnotator implements Annotator { - - @Override - public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { - ProjectEnv.withProject(element.getProject(), () -> { - doAnnotate(element, holder); - return null; - }); - } - - void doAnnotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { - if (element instanceof XmlTag) { - XmlTag tag = (XmlTag) element; - - XmlTagInfo tagInfo = getTagInfo(tag); - if (tagInfo == null) - return; - - // 父节点就不合法,则本节点无需处理 - 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) - return; - String attrName = XmlPsiHelper.getAttrName((XmlAttributeValue) element); - if (!StringHelper.isBlank(attrName)) { - XDefTypeDecl attrType = getAttrType(attrName, tagInfo); - if (attrType != null) { - checkAttrValue(holder, attrType, (XmlAttributeValue) element); - } - } - } - } - - private XmlTagInfo getTagInfo(PsiElement element) { - return XDefPsiHelper.getTagInfo(element); - } - - private boolean 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; - } - - 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(); - } - } else { - for (XmlAttribute attr : tag.getAttributes()) { - checkAttr(holder, attr, tagInfo); - } - - return checkValue(holder, tag, defNode); - } - - return true; - } - - private void checkAttr(AnnotationHolder holder, @NotNull XmlAttribute attr, - XmlTagInfo tagInfo) { - String attrName = attr.getName(); - if (StringHelper.isBlank(attrName)) - 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()); - } - } - - private XDefTypeDecl getAttrType(String attrName, XmlTagInfo tagInfo) { - IXDefNode defNode = tagInfo.getDefNode(); - - XDefTypeDecl attrType = null; - if (attrName.startsWith("xdsl:")) { - attrName = "x:" + attrName.substring("xdsl:".length()); - } - IXDefAttribute defAttr = null; - // 识别系统保留名字空间 - if (attrName.startsWith("x:")) { - if (tagInfo.getDslNode() != null) { - defAttr = tagInfo.getDslNode().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); - } - } - 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(); - } - 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(); - } - } - } - } - - private boolean 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 false; - } - } - 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(); - } - } - - return true; - } - - String getTagValue(XmlTag tag) { - if (XmlPsiHelper.hasChild(tag)) { - return null; - } - return StringHelper.unescapeXml(tag.getValue().getText()); - } - - XmlElement getStartTagName(XmlTag tag) { - XmlElement element = XmlTagUtil.getStartTagNameElement(tag); - if (element == null) { - element = tag; - } - return element; - } -} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..bc86d2d0dc6e60248fda74e8e5ec317f6b989e62 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangAnnotator.java @@ -0,0 +1,291 @@ +/* + * 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.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; +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.XmlElement; +import com.intellij.psi.xml.XmlTag; +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.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +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 { + + /** + * 注意事项: + * + */ + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (!(element instanceof XmlElement)) { + return; + } + + ProjectEnv.withProject(element.getProject(), () -> { + try { + doAnnotate(holder, element); + } catch (Exception e) { + String msg = e.getMessage() != null ? e.getMessage() : e.getClass().getName(); + + holder.newAnnotation(HighlightSeverity.WARNING, msg) + .highlightType(ProblemHighlightType.WARNING) + .create(); + } + return null; + }); + } + + private void doAnnotate(@NotNull AnnotationHolder holder, @NotNull PsiElement element) { + checkReferences(holder, element); + + if (element instanceof XLangTag tag) { + checkTag(holder, tag); + } // + else if (element instanceof XLangAttribute attr) { + checkAttr(holder, attr); + } // + else if (element instanceof XLangAttributeValue attrValue) { + checkAttrValue(holder, attrValue); + } + // Note: Annotator 不会触发对 XLangTextToken 等 AST 叶子节点的检查, + // 需通过其父节点(XLangText 等)触发对 XLangTextToken 的检查 + else if (element instanceof XLangText text) { + for (XLangTextToken token : text.getTextTokens()) { + doAnnotate(holder, token); + } + } + } + + private void checkReferences(@NotNull AnnotationHolder holder, @NotNull PsiElement element) { + for (PsiReference reference : element.getReferences()) { + if (!(reference instanceof XLangReference ref)) { + continue; + } + + TextRange textRange = ref.getAbsoluteRange(); + PsiElement target = ref.resolve(); + + if (target instanceof NopVirtualFile vfs && vfs.forFileChildren()) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(textRange) + .textAttributes(DefaultLanguageHighlighterColors.DOC_COMMENT_TAG_VALUE) + .create(); + } + + String msg = ref.getUnresolvedMessage(); + if (target == null && msg != null) { + _errorAnnotation(holder, textRange, msg); + } + } + } + + private void checkTag(@NotNull AnnotationHolder holder, @NotNull XLangTag tag) { + if (tag.getSchemaDefNode() != null) { + checkTagValue(holder, tag); + return; + } + + XLangTag parentTag = tag.getParentTag(); + if ((parentTag != null && parentTag.isXdefValueSupportBody()) // + || tag.isAllowedUnknownTag() // + ) { + return; + } + + 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.getSchemaDefNodeXdefValue(); + 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()); + } + 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); + + String stdDomain = xdefValue.getStdDomain(); + if (tag.isXlibSourceNode()) { + stdDomain = "xpl"; + } + + checkStdDomain(holder, textRange, stdDomain, loc, "body", bodyText); + } + } + + private void checkAttr(@NotNull AnnotationHolder holder, @NotNull XLangAttribute attr) { + XmlElement attrNameElement = attr.getNameElement(); + if (attrNameElement == null // + || "xmlns".equals(attrNameElement.getText()) // + || "xmlns".equals(attr.getNamespacePrefix()) // + ) { + return; + } + + if (attr.getDefAttr() == null) { + errorAnnotation(holder, + attrNameElement.getTextRange(), + "xlang.annotation.attr.not-defined", + attr.getName()); + } + } + + private void checkAttrValue(@NotNull AnnotationHolder holder, @NotNull XLangAttributeValue attrValue) { + XLangAttribute attr = attrValue.getParentAttr(); + IXDefAttribute defAttr = attr != null ? attr.getDefAttr() : null; + if (defAttr == null) { + return; + } + + String attrName = attr.getName(); + String attrValueText = attrValue.getValue(); + TextRange attrValueTextRange = attrValue.getValueTextRange(); + + XDefTypeDecl defAttrType = defAttr.getType(); + if (StringHelper.isEmpty(attrValueText)) { + if (defAttrType.isMandatory()) { + errorAnnotation(holder, attrValue.getTextRange(), "xlang.annotation.attr.value-required", attrName); + } + return; + } + + // Note: dict/enum 的有效值检查由 PsiReference 处理 + SourceLocation loc = XmlPsiHelper.getLocation(attrValue); + checkStdDomain(holder, attrValueTextRange, defAttrType.getStdDomain(), loc, attrName, attrValueText); + } + + 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); + if (XplParseHelper.hasExpr(propValue)) { + return; + } + + try { + domainHandler.validate(loc, propName, propValue, IValidationErrorCollector.THROW_ERROR); + } catch (Exception e) { + errorAnnotation(holder, textRange, e); + } + } + + private XmlElement getStartTagName(XmlTag tag) { + XmlElement element = XmlTagUtil.getStartTagNameElement(tag); + + return element != null ? element : tag; + } + + private void errorAnnotation(AnnotationHolder holder, TextRange textRange, String msgKey, Object... msgParams) { + String msg = NopPluginBundle.message(msgKey, msgParams); + + _errorAnnotation(holder, textRange, msg); + } + + 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(); + } + + _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/annotator/XLangHighlightRangeExtension.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangHighlightRangeExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..b29ea3e27276564e21ff20ef4bff8f406160e663 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/annotator/XLangHighlightRangeExtension.java @@ -0,0 +1,28 @@ +/* + * 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.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/completion/XLangCompletionContributor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/completion/XLangCompletionContributor.java index 551393bc09d7925742890b15aea5432a6f0b3256..1a90680c632e2d2007f550d1b22629decae40412 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,34 @@ 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/ + * + * @deprecated 通过 {@link com.intellij.psi.PsiReference#getVariants PsiReference#getVariants} 实现补全 */ +@Deprecated public class XLangCompletionContributor extends CompletionContributor implements DumbAware { - public XLangCompletionContributor() { } @@ -61,19 +64,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 +94,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 +117,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.getDslNode(); - } - 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 +234,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); } -} \ No newline at end of file +} 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 5b18653e2f2b000bfb57dd797fdb9d75ac531f1b..0000000000000000000000000000000000000000 --- 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) { // todo! -// 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/doc/XLangDocumentationProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/doc/XLangDocumentationProvider.java index cc42cb03886264fe1e2cfa76911ec748f6de977c..02d895b1aa1e2113febf5fed72a01c8905e8312f 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,194 +7,122 @@ */ 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 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.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; -import io.nop.idea.plugin.utils.XmlPsiHelper; -import io.nop.idea.plugin.utils.XmlTagInfo; +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.ProjectFileHelper; 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 org.jetbrains.annotations.NotNull; - import jakarta.annotation.Nullable; -import java.util.Objects; + +import static com.intellij.psi.xml.XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN; +import static com.intellij.psi.xml.XmlTokenType.XML_NAME; public class XLangDocumentationProvider extends AbstractDocumentationProvider { + /** + * 文档生成函数 + *

+ * 默认鼠标移动时的文档也由该函数生成 {@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 generateDoc(PsiElement element, @Nullable PsiElement originalElement) { - return ProjectEnv.withProject(element.getProject(), () -> { - return doGenerate(element, originalElement); - }); - } + public @Nullable String generateDoc(PsiElement resolvedElement, @Nullable PsiElement srcElement) { + if (srcElement == null) { + return null; + } - 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()); - } - doc.setDesc(option.getDescription()); - - return doc.toString(); + XLangDocumentation doc = null; + IElementType elementType = srcElement.getNode().getElementType(); + if (elementType == XML_NAME) { + doc = generateDocForXmlName(srcElement); + } // + else if (elementType == XML_ATTRIBUTE_VALUE_TOKEN) { + doc = generateDocForXmlAttributeValue(srcElement); } - return null; + return doc != null ? doc.genDoc() : null; } /** - * Provides documentation when a Simple Language element is hovered with the mouse. + * 为文档链接中的 {@link DocumentationManagerProtocol#PSI_ELEMENT_PROTOCOL} + * 协议路径创建对应的 {@link PsiElement} */ @Override - public @Nullable - String generateHoverDoc(@NotNull PsiElement element, @Nullable PsiElement originalElement) { - return generateDoc(element, originalElement); + public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) { + return XLangDocumentation.createElementForLink(context, link); } - /** - * 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; - } + /** 为 xml 标签名和属性名生成文档 */ + private XLangDocumentation generateDocForXmlName(PsiElement element) { + PsiElement parent = element.getParent(); - /** 对于多行文本,行首的 > 将被去除后,再按照 markdown 渲染得到 html 代码 */ - public static String markdown(String text) { - text = text.replaceAll("(?m)^> ",""); - text = MarkdownHelper.renderHtml(text); + 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(); - return text; - } + doc = tag != null ? tag.getAttrDocumentation(attrName) : null; + } - static class DocInfo { - String title; - String stdDomain; - String desc; + return doc; + } - DocInfo() { + /** 为 xml 属性值生成文档 */ + private XLangDocumentation generateDocForXmlAttributeValue(PsiElement element) { + XLangAttribute attr = PsiTreeUtil.getParentOfType(element, XLangAttribute.class); + if (attr == null) { + return null; } - DocInfo(IXDefNode defNode) { - this(defNode.getXdefValue()); + IXDefAttribute defAttr = attr.getDefAttr(); + XDefTypeDecl defAttrType = defAttr != null ? defAttr.getType() : null; + if (defAttrType == null) { + return null; } - DocInfo(IXDefAttribute attr) { - this(attr.getType()); + if (!XDefConstants.STD_DOMAIN_DICT.equals(defAttrType.getStdDomain())) { + return null; } - DocInfo(XDefTypeDecl type) { - this.stdDomain = type != null ? type.getStdDomain() : null; + DictBean dictBean = ProjectFileHelper.loadDict(element, defAttrType.getOptions()); + DictOptionBean option = dictBean != null ? dictBean.getOptionByValue(attr.getValue()) : null; + if (option == null) { + return null; } - public void setTitle(String title) { - this.title = title; - } + String value = option.getStringValue(); + String label = option.getLabel(); - public void setDesc(String desc) { - this.desc = desc; - } + XLangDocumentation doc = new XLangDocumentation(dictBean); + doc.setMainTitle(value); - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - if (!StringHelper.isBlank(this.title)) { - sb.append("

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

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

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

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

"); - } - - sb.append(markdown(this.desc)); - } - - return !sb.isEmpty() ? sb.toString() : null; + if (label != null && !Objects.equals(label, value)) { + doc.setSubTitle(label.startsWith(value + '-') ? label.substring(value.length() + 1) : label); } + doc.setDesc(option.getDescription()); + + return doc; } } 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 0000000000000000000000000000000000000000..f40eae04027ccfab5b32fe5f03c48a068be2955b --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangDocumentation.java @@ -0,0 +1,224 @@ +/* + * 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.stream.Collectors; + +import com.intellij.codeInsight.documentation.DocumentationManagerProtocol; +import com.intellij.ide.highlighter.JavaHighlightingColors; +import com.intellij.lang.documentation.DocumentationMarkup; +import com.intellij.lang.documentation.DocumentationSettings; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.TextAttributesKey; +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.CollectionHelper; +import io.nop.commons.util.StringHelper; +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 说明文档 + * + * @author flytreeleft + * @date 2025-07-18 + */ +public class XLangDocumentation { + private static final String LINK_PREFIX_NOP_VFS = "nop-vfs:"; + + String mainTitle; + String subTitle; + + String stdDomain; + String defaultValue; + String[] modifiers; + + String desc; + String path; + + public XLangDocumentation(ISourceLocationGetter locGetter) { + this(locGetter, null); + } + + public XLangDocumentation(IXDefNode defNode) { + this(defNode, defNode.getXdefValue()); + } + + public XLangDocumentation(IXDefAttribute defAttr) { + this(defAttr, defAttr.getType()); + } + + XLangDocumentation(ISourceLocationGetter locGetter, XDefTypeDecl type) { + this.path = XmlPsiHelper.getNopVfsPath(locGetter); + + if (type != null) { + this.modifiers = new String[] { + type.isMandatory() + ? NopPluginBundle.message("xlang.doc.flag.required") + : NopPluginBundle.message("xlang.doc.flag.option"), // + type.isInternal() || type.isDeprecated() + ? NopPluginBundle.message("xlang.doc.flag.internal") + : null, // + type.isAllowCpExpr() ? NopPluginBundle.message("xlang.doc.flag.allow-cp-expr") : null, // + }; + + this.stdDomain = type.getStdDomain(); + if (type.getOptions() != null) { + this.stdDomain += ':' + type.getOptions(); + } + + this.defaultValue = StringHelper.toString(type.getDefaultValue(), null); + if (this.defaultValue == null && !CollectionHelper.isEmpty(type.getDefaultAttrNames())) { + this.defaultValue = type.getDefaultAttrNames() + .stream() + .map((name) -> '@' + name) + .collect(Collectors.joining("|")); + } + } + } + + public void setMainTitle(String mainTitle) { + this.mainTitle = mainTitle; + } + + public void setSubTitle(String subTitle) { + this.subTitle = subTitle; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public String genDoc() { + StringBuilder sb = new StringBuilder(); + 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); + } + + { + sb.append(DocumentationMarkup.DEFINITION_START); + + appendStyledSpan(sb, resolveAttributes(JavaHighlightingColors.KEYWORD), mainTitle); + if (StringHelper.isNotBlank(defaultValue)) { + appendStyledSpan(sb, + resolveAttributes(JavaHighlightingColors.LINE_COMMENT), + " (=" + defaultValue + ')'); + } + if (StringHelper.isNotBlank(subTitle)) { + sb.append(' '); + appendStyledSpan(sb, resolveAttributes(JavaHighlightingColors.CLASS_NAME_ATTRIBUTES), subTitle); + } + + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + StringBuilder sb1 = new StringBuilder(); + if (modifiers != null) { + for (String modifier : modifiers) { + if (modifier != null) { + appendStyledSpan(sb1, + resolveAttributes(JavaHighlightingColors.INSTANCE_FIELD_ATTRIBUTES), + modifier); + } + } + } + if (stdDomain != null) { + if (!sb1.isEmpty()) { + sb1.append(' '); + } + appendStyledSpan(sb1, resolveAttributes(JavaHighlightingColors.LOCAL_VARIABLE_ATTRIBUTES), stdDomain); + } + if (!sb1.isEmpty()) { + sb.append("\n\n").append(sb1); + } + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + + sb.append(DocumentationMarkup.DEFINITION_END); + } + + 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)); + } + + // source from com.intellij.codeInsight.javadoc.JavaDocHighlightingManagerImpl#resolveAttributes + private static @NotNull TextAttributes resolveAttributes(@NotNull TextAttributesKey attributesKey) { + return EditorColorsManager.getInstance().getGlobalScheme().getAttributes(attributesKey); + } + // >>>>>>>>>>>>>>>>>>>>>>> +} 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 0000000000000000000000000000000000000000..5ad000d1d47e876917294bb8ed896bf3fdc15f58 --- /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/XLangFileTypeDetector.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileTypeDetector.java index 161eee68d02f4e7ed551c920c5036963fa043145..5cbbf292c41fe376c8c01d9825195e4b63d02abd 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/lang/XLangLanguage.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangLanguage.java index 29bbe8ea2ee3e0275191a44fbe44d3b83705dbdf..bc5ff763d59a116b35c000b15c3f55031f0609bc 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/XLangParserDefinition.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangParserDefinition.java index d2ed5247d2e9f396c960a78d59b78f298a4ce654..a207a5f058e6d0878cad978b1515110d6a1e8596 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,140 @@ */ 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.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; -public class XLangParserDefinition extends XMLParserDefinition { +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 节点包装 + *

+ * 从 {@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) { + 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(); + + /** 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 new file mode 100644 index 0000000000000000000000000000000000000000..c94e4b933d9813bcfc4369c29f2d508f4f5bb2dd --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangScriptLanguageInjector.java @@ -0,0 +1,68 @@ +/* + * 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 com.intellij.lang.Language; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.InjectedLanguagePlaces; +import com.intellij.psi.LanguageInjector; +import com.intellij.psi.PsiLanguageInjectionHost; +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; + +/** + * XScript 脚本的 {@link LanguageInjector} + *

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

+ * 通过 InjectedLanguageManager.getInstance(host.getProject()).findInjectedElementAt(host, offset); + * 可以从目标元素内查找到已注入的其他语言节点 + * + * @author flytreeleft + * @date 2025-06-25 + */ +public class XLangScriptLanguageInjector implements LanguageInjector { + + @Override + public void getLanguagesToInject( + @NotNull PsiLanguageInjectionHost host, @NotNull InjectedLanguagePlaces registrar + ) { + // 针对仅包含文本内容的 Xpl 类型节点(xdef:value=xpl*) + if (!(host instanceof XLangText) // + || !(host.getParent() instanceof XLangTag tag) // + || !tag.isXplDefNode() // + || (!XplConstants.TAG_C_SCRIPT.equals(tag.getName()) // + && !tag.isXlibSourceNode() // TODO 暂时仅针对 c:script/source 标签做内嵌代码解析 + ) // + || tag.hasChildTag() // + ) { + return; + } + + String langType = tag.getAttributeValue("lang"); + if (StringHelper.isEmpty(langType)) { + langType = "xlang"; + } + + Language lang = switch (langType) { + case "xlang" -> XLangScriptLanguage.INSTANCE; + default -> null; + }; + + 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/XLangVarDecl.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarDecl.java new file mode 100644 index 0000000000000000000000000000000000000000..f06b4b2489c2fac6a1714296560ab06a970d0d4b --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarDecl.java @@ -0,0 +1,20 @@ +/* + * 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 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 0000000000000000000000000000000000000000..b7bf62a5c3573a67e53b4bdb97d34d13b3baa145 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangVarScope.java @@ -0,0 +1,27 @@ +/* + * 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 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/psi/XLangAttribute.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..2c7e35444b74e91f22e23427b8fb4c222602c50d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttribute.java @@ -0,0 +1,90 @@ +/* + * 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.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 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; + +/** + * 属性,由名字(含名字空间)、等号和 {@link XLangAttributeValue} 组成 + * + * @author flytreeleft + * @date 2025-07-09 + */ +public class XLangAttribute extends XmlAttributeImpl { + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + getElementType() + "('" + getName() + "')"; + } + + public XLangTag getParentTag() { + return (XLangTag) getParent(); + } + + @Override + public PsiReference @NotNull [] getReferences(@NotNull PsiReferenceService.Hints hints) { + // 参考 XmlAttributeDelegate#getDefaultReferences + String ns = getNamespacePrefix(); + String name = getLocalName(); + + if (name.isEmpty() || isNamespaceDeclaration()) { + return super.getReferences(hints); + } + + // 保留对名字空间的引用,以支持对其做高亮、重命名等 + 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); + 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 }; + } + + /** 获取当前属性在元模型中的定义 */ + public IXDefAttribute getDefAttr() { + XLangTag tag = getParentTag(); + if (tag == null) { + return null; + } + + String ns = getNamespacePrefix(); + String attrName = getName(); + boolean hasXDslNs = !ns.isEmpty() && ns.equals(tag.getXDslKeys().NS); + + IXDefAttribute defAttr; + // 取 xdsl.xdef 中声明的属性 + if (hasXDslNs) { + defAttr = tag.getXDslDefNodeAttr(attrName); + } // + else { + defAttr = tag.getSchemaDefNodeAttr(attrName); + } + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..99b4ba6555170347f726ff97cd61b5546664d4b5 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangAttributeValue.java @@ -0,0 +1,131 @@ +/* + * 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.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.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.xdsl.XDslKeys; +import io.nop.xlang.xpl.utils.XplParseHelper; +import org.jetbrains.annotations.NotNull; + +/** + * 属性值,由引号和 {@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 + */ +public class XLangAttributeValue extends XmlAttributeValueImpl { + + @Override + public String toString() { + 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(); + if (StringHelper.isEmpty(attrValue) || XplParseHelper.hasExpr(attrValue)) { + return PsiReference.EMPTY_ARRAY; + } + + XLangAttribute attr = getParentAttr(); + if (attr == null) { + return PsiReference.EMPTY_ARRAY; + } + + IXDefAttribute defAttr = attr.getDefAttr(); + // 对于未定义属性,不做引用识别 + if (defAttr == null || (defAttr.isUnknownAttr() && defAttr.getType().getStdDomain().equals("any"))) { + //return PsiReference.EMPTY_ARRAY; + // Note: 临时支持对 xpl 内置函数的 vfs 引用识别 + return XLangReferenceHelper.getReferencesFromText(this, attrValue); + } + + // 根据属性名,从属性值中查找引用 + PsiReference[] refs = getReferencesByAttrName(attr, attrValue); + if (refs != null) { + return refs; + } + + // 根据属性定义类型,从属性值中查找引用 + refs = XLangReferenceHelper.getReferencesByDefType(this, attrValue, defAttr.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)) { + 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) // + || xdefKeys.ORDER_ATTR.equals(attrName) // + ) { + return new PsiReference[] { + new XLangParentTagAttrReference(this, attrValueTextRange, attrValue) + }; + } // + else if (xdefKeys.NAME.equals(attrName)) { + // 与根节点上的 xdef:bean-package 组成 class + XLangTag rootTag = tag.getRootTag(); + + 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) + }; + } + + return null; + } +} 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 0000000000000000000000000000000000000000..6253cd802e8c004c0eca7acb9becfabe06fe4934 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangDocument.java @@ -0,0 +1,23 @@ +/* + * 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.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 0000000000000000000000000000000000000000..c82227ab3cdbb7c4cbed6a40706dec4a73f3e16d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTag.java @@ -0,0 +1,928 @@ +/* + * 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.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; +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.util.IncorrectOperationException; +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.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; +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; +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; +import io.nop.xlang.xdsl.XDslKeys; +import io.nop.xlang.xpl.XplConstants; +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; + +/** + * {@link XNode} 标签(其名字含名字空间) + *

+ * 负责识别标签、属性、属性值的引用 + * + * @author flytreeleft + * @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); + + private SchemaMeta schemaMeta; + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + getElementType() + "('" + getName() + "')"; + } + + @Override + public void clearCaches() { + this.schemaMeta = null; + + super.clearCaches(); + } + + @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); + } + + /** 当前标签是否有子标签 */ + public boolean hasChildTag() { + return getNode().findChildByType(XML_TAG) != null; + } + + /** 获取当前标签内的文本内容(特殊符号已转义) */ + public @NotNull String getBodyText() { + XLangText text = (XLangText) findPsiChildByType(XML_TEXT); + + 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<>(); + + XlibTagMeta xlibTag = getXlibTagMeta(); + + // 参考 XmlTagDelegate#getReferencesImpl + PsiReference[] xmlRefs = super.getReferences(hints); + // Note: 仅保留对名字空间的引用,以支持对其做高亮、重命名等 + for (PsiReference ref : xmlRefs) { + // 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), // + 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); + + PsiReference ref; + if (xlibTag == null) { + ref = new XLangTagReference(this, textRange); + } else { + ref = new XLangXlibTagReference(this, textRange, xlibTag.tagName, xlibTag.xlibPath); + } + + refs.add(ref); + } + + return refs.toArray(PsiReference.EMPTY_ARRAY); + } + + /** @see SchemaMeta#getSchemaDef() */ + private IXDefinition getSchemaDef() { + return getSchemaMeta().getSchemaDef(); + } + + /** @see SchemaMeta#getSchemaDefNode() */ + public IXDefNode getSchemaDefNode() { + return getSchemaMeta().getSchemaDefNode(); + } + + /** + * 获取当前标签上指定属性在元模型中的定义 + *

+ * 在元元模型 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 的属性在固定的名字空间 xdef 中声明 + else { + attrName = changeNamespace(attrName, getXDefKeys().NS, XDefKeys.DEFAULT.NS); + } + + IXDefAttribute attr = getXDefNodeAttr(getSchemaDefNode(), attrName); + + if (attr == null || attr.isUnknownAttr()) { + XlibTagMeta xlibTag = getXlibTagMeta(); + + if (xlibTag != null) { + return xlibTag.getAttribute(attrName); + } + } + return attr; + } + + /** + * 获取 {@link #getSchemaDefNode()} 节点的 {@link XDefKeys#VALUE xdef:value}, + * 即,其子节点(包括文本节点)对应的{@link XDefTypeDecl 类型} + */ + public XDefTypeDecl getSchemaDefNodeXdefValue() { + IXDefNode defNode = getSchemaDefNode(); + + return defNode != null ? defNode.getXdefValue() : null; + } + + /** @see SchemaMeta#getXDslDefNode() */ + public IXDefNode getXDslDefNode() { + return getSchemaMeta().getXDslDefNode(); + } + + /** 获取当前标签上指定属性在 xdsl.xdef 中的定义 */ + public IXDefAttribute getXDslDefNodeAttr(String attrName) { + // Note: xdsl.xdef 的属性在固定的名字空间 x 中声明 + attrName = changeNamespace(attrName, getXDslKeys().NS, XDslKeys.DEFAULT.NS); + + return getXDefNodeAttr(getXDslDefNode(), attrName); + } + + /** @see SchemaMeta#getSelfDefNode() */ + public IXDefNode getSelfDefNode() { + return getSchemaMeta().getSelfDefNode(); + } + + /** @see SchemaMeta#getXDefKeys() */ + public XDefKeys getXDefKeys() { + return getSchemaMeta().getXDefKeys(); + } + + /** @see SchemaMeta#getXDslKeys() */ + public XDslKeys getXDslKeys() { + return getSchemaMeta().getXDslKeys(); + } + + /** @see SchemaMeta#isInXDefXDef() */ + private boolean isInXDefXDef() { + return getSchemaMeta().isInXDefXDef(); + } + + /** @see SchemaMeta#isXplDefNode() */ + public boolean isXplDefNode() { + return getSchemaMeta().isXplDefNode(); + } + + /** @see SchemaMeta#isXlibSourceNode() */ + public boolean isXlibSourceNode() { + return getSchemaMeta().isXlibSourceNode(); + } + + /** 当前标签是否允许拥有子标签 */ + public boolean isAllowedChildTag() { + IXDefNode defNode = getSchemaDefNode(); + if (defNode == null) { + return false; + } else if (defNode.hasChild()) { + return true; + } + + return isXdefValueSupportBody(); + } + + /** 当前标签的 {@link #getSchemaDefNodeXdefValue() xdef:value} 类型是否支持内嵌节点 */ + public boolean isXdefValueSupportBody() { + XDefTypeDecl xdefValue = getSchemaDefNodeXdefValue(); + + 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); + } + + /** 若当前标签对应的是 xlib 的函数节点,则返回该函数节点信息 */ + public 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; + 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; + if (lib == null) { + 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); + } + + /** 获取当前标签的说明文档 */ + 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; + } + + XlibTagMeta xlibTag = getXlibTagMeta(); + if (xlibTag != null) { + return xlibTag.getDocumentation(); + } + + 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 defAttr = getXDefNodeAttr(defNode, attrName); + if (defAttr == null) { + return null; + } + + if (defAttr.isUnknownAttr()) { + XlibTagMeta xlibTag = getXlibTagMeta(); + + if (xlibTag != null) { + return xlibTag.getAttrDocumentation(attrName); + } + } + + XLangDocumentation doc = new XLangDocumentation(defAttr); + doc.setMainTitle(mainTitle); + + IXDefComment nodeComment = defNode.getComment(); + if (nodeComment != null) { + IXDefSubComment attrComment = nodeComment.getSubComments().get(attrName); + if (attrComment == null && defAttr.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:"; + 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); + + return at; + } + + if (xdefNode == null) { + return null; + } + + IXDefAttribute attr = xdefNode.getAttribute(attrName); + if (attr != null) { + return attr; + } + + // Note: 在 IXDefNode 中,对 xdef:unknown-attr 只记录了类型,并没有 IXDefAttribute 实体, + // 其处理逻辑见 XDefinitionParser#parseNode + XDefTypeDecl xdefUnknownAttrType = xdefNode.getXdefUnknownAttr(); + if (xdefUnknownAttrType != null) { + XDefAttribute at = new XDefAttribute() { + @Override + public boolean isUnknownAttr() { + return true; + } + }; + + at.setName(getXDefKeys().UNKNOWN_ATTR); + at.setType(xdefUnknownAttrType); + + // Note: 在需要时,通过节点位置再定位具体的属性位置 + 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; + } + + 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; + } + + if (StringHelper.startsWithNamespace(name, fromNs)) { + return toNs + ':' + name.substring(fromNs.length() + 1); + } + return name; + } + + 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; + + // Note: 避免后续访问成员变量出现 NPE 问题 + return UNKNOWN_SCHEMA_META; + } + } + return schemaMeta; + } + + private SchemaMeta createSchemaMeta() { + XLangTag parentTag = getParentTag(); + if (parentTag == null) { + return createSchemaMetaForRootTag(this); + } + + String tagName = getName(); + String tagNs = getNamespacePrefix(); + + Project project = getProject(); + 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; + } + + 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); + + Supplier 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 资源 + IXDefinition def = XDefPsiHelper.loadSchema(rootTag.getContainingFile()); + selfDef = () -> def; + } + } + + Project project = rootTag.getProject(); + + 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 (parent.isInXDefXDef() && XDefKeys.DEFAULT.NS.equals(tagNs)) { + defNode = parentDefNode.getXdefUnknownTag(); + } + // 其余的,则将标签的 xdef 名字空间固定为名字 xdef + else { + XDefKeys xdefKeys = parent.getXDefKeys(); + String newTagName = changeNamespace(tagName, xdefKeys.NS, XDefKeys.DEFAULT.NS); + + 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(); + } + + @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 class UnknownSchemaMeta extends SchemaMeta { + + UnknownSchemaMeta() { + super(null, null); + } + + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + @Override + public @Nullable IXDefinition getSchemaDef() { + return null; + } + + @Override + public @Nullable IXDefNode getSchemaDefNode() { + return null; + } + + @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; + } + + /** + * 当前标签所在的元模型(在 *.xdef 中定义) + *

+ * 允许元模型不存在,以支持检查 xdsl.xdef 对应的节点 + */ + public abstract @Nullable IXDefinition getSchemaDef(); + + /** 当前标签在 {@link #getSchemaDef()} 中所对应的节点 */ + public abstract @Nullable IXDefNode getSchemaDefNode(); + + /** + * 当前标签在 xdsl 模型(xdsl.xdef)中所对应的节点。 + * 注:所有 DSL 模型的节点均与 xdsl.xdef 的节点存在对应 + */ + public abstract @Nullable IXDefNode getXDslDefNode(); + + /** 当当前标签定义在 *.xdef 文件中时,需记录该元模型 */ + 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 abstract @NotNull XDefKeys getXDefKeys(); + + /** + * /nop/schema/xdsl.xdef 对应的 {@link XDslKeys}。 + * 在 DSL 模型(含元模型)中均有设置,如 xmlns:x="/nop/schema/xdsl.xdef" + */ + public abstract @NotNull XDslKeys getXDslKeys(); + + /** 当前标签是否在元元模型 xdef.xdef 中 */ + 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 boolean isInXDslXDef() { + IXDefinition def = getSelfDef(); + + // Note: 在单元测试中只能基于内容做判断,而不是 vfs 路径 + return def != null // + && def.getXdefCheckNs().contains(XDslKeys.DEFAULT.NS) // + && !XDslKeys.DEFAULT.equals(getXDslKeys()); + } + + /** 当前标签是否对应 Xlib 的 source 节点 */ + public boolean isXlibSourceNode() { + IXDefNode defNode = getSchemaDefNode(); + if (defNode == null) { + return false; + } + + String defPath = XmlPsiHelper.getNopVfsPath(defNode); + + 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 节点处理 + 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/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 0000000000000000000000000000000000000000..69cef1d05c31b61cfdc0da91b62501cc507fbb06 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangText.java @@ -0,0 +1,90 @@ +/* + * 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.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} 的直接叶子节点: + *

+ * 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 + */ +public class XLangText extends XmlTextImpl { + + @Override + public String toString() { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..d8f0881f8b2c82f07f48e1a68ecf7b257cd347ad --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangTextToken.java @@ -0,0 +1,75 @@ +/* + * 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.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; + +/** + * CDATA 节点({@link com.intellij.psi.xml.XmlElementType#XML_CDATA XML_CDATA})中的全部文本, + * 或者 {@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 + */ +public class XLangTextToken extends XmlTokenImpl { + + public XLangTextToken(@NotNull IElementType type, CharSequence text) { + super(type, text); + } + + @Override + 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.getSchemaDefNodeXdefValue(); + 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/psi/XLangValueToken.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed940d13e14562a5eafcf9059082c4dfa8eba41 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/psi/XLangValueToken.java @@ -0,0 +1,37 @@ +/* + * 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.psi; + +import com.intellij.psi.impl.source.xml.XmlTokenImpl; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * {@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 + */ +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/lang/reference/XLangAttributeReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java new file mode 100644 index 0000000000000000000000000000000000000000..67b1dba7f147af64185ccf69b08e72f62d049888 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangAttributeReference.java @@ -0,0 +1,193 @@ +/* + * 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.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; +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 { + private final IXDefAttribute defAttr; + + public XLangAttributeReference(XLangAttribute myElement, TextRange myRangeInElement, IXDefAttribute defAttr) { + super(myElement, myRangeInElement); + this.defAttr = defAttr; + } + + @Override + public @Nullable PsiElement resolveInner() { + 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() { + // Note: 在自动补全阶段,DSL 结构很可能是不完整的,只能从 xml 角度做分析 + 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); + } + // 引入可能的 xlib 标签函数的参数 + addDefAttr(result, tag.getXlibTagMeta(), existAttrNames); + + return result.stream() // + .sorted((a, b) -> XLangReferenceHelper.XLANG_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 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) { + 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/XLangDictOptionReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java new file mode 100644 index 0000000000000000000000000000000000000000..39feaf16856f3106387abb6d83ec5874b1d8dca4 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangDictOptionReference.java @@ -0,0 +1,108 @@ +/* + * 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.codeInsight.lookup.LookupElement; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import io.nop.api.core.beans.DictBean; +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.utils.LookupElementHelper; +import io.nop.idea.plugin.utils.ProjectFileHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对字典选项的引用 + * + * @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, Object dictOptionValue + ) { + super(myElement, myRangeInElement); + this.dictName = dictName; + this.dictOptionValue = dictOptionValue; + } + + public DictBean getDict() { + if (dictBean == null) { + dictBean = ProjectFileHelper.loadDict(myElement, dictName); + } + return dictBean; + } + + @Override + public @Nullable PsiElement resolveInner() { + DictBean dict = getDict(); + if (dict == null) { + String msg = NopPluginBundle.message("xlang.annotation.reference.dict-not-found", dictName); + setUnresolvedMessage(msg); + + return null; + } + + if (dict instanceof EnumDictBean) { + EnumDictOptionBean dictOpt = (EnumDictOptionBean) dict.getOptionByValue(dictOptionValue); + + if (dictOpt != null) { + return dictOpt.target; + } + + String msg = NopPluginBundle.message("xlang.annotation.reference.enum-option-not-defined", + dictOptionValue, + dict.getValues()); + setUnresolvedMessage(msg); + + return null; + } // + else { + NopVirtualFile target = XLangReferenceHelper.createNopVfsForDict(myElement, dictName, dictOptionValue); + + if (target.hasEmptyChildren()) { + String path = target.getPath(); + String msg = NopPluginBundle.message("xlang.annotation.reference.dict-option-not-defined", + dictOptionValue, + path); + setUnresolvedMessage(msg); + + return null; + } + return target; + } + } + + @Override + public Object @NotNull [] getVariants() { + DictBean dict = getDict(); + if (dict == null) { + return LookupElement.EMPTY_ARRAY; + } + + return dict.getOptions() + .stream() + .filter((opt) -> !opt.isInternal() && !opt.isDeprecated()) + .map(LookupElementHelper::lookupDictOpt) + .sorted((a, b) -> XLangReferenceHelper.XLANG_NAME_COMPARATOR.compare(a.getLookupString(), + b.getLookupString())) + .toArray(); + } + +} 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 0000000000000000000000000000000000000000..6cd8968f4b9d730fa7624532d8e78a0e9ea0fc8b --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangParentTagAttrReference.java @@ -0,0 +1,86 @@ +/* + * 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.util.PsiTreeUtil; +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 io.nop.idea.plugin.utils.XmlPsiHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对父标签上的属性的引用 + * + * @author flytreeleft + * @date 2025-07-13 + */ +public class XLangParentTagAttrReference extends XLangReferenceBase { + private final String attrName; + + public XLangParentTagAttrReference(XmlElement myElement, TextRange myRangeInElement, String attrName) { + super(myElement, myRangeInElement); + this.attrName = attrName; + } + + private XLangTag getParentTag() { + return PsiTreeUtil.getParentOfType(myElement, XLangTag.class); + } + + private XLangAttribute getParentAttr() { + return PsiTreeUtil.getParentOfType(myElement, XLangAttribute.class); + } + + @Override + public @Nullable PsiElement resolveInner() { + XLangTag tag = getParentTag(); + if (tag == null) { + return null; + } + + XLangAttribute target = (XLangAttribute) tag.getAttribute(attrName); + + if (target == null) { + String msg = NopPluginBundle.message("xlang.annotation.reference.parent-tag-attr-not-found", attrName); + setUnresolvedMessage(msg); + } + // 不能引用属性自身 + else if (target == getParentAttr()) { + String msg = NopPluginBundle.message("xlang.annotation.reference.parent-tag-attr-self-referenced", attrName); + setUnresolvedMessage(msg); + + return null; + } + + return target; + } + + @Override + public Object @NotNull [] getVariants() { + // Note: 在自动补全阶段,DSL 结构很可能是不完整的,只能从 xml 角度做分析 + XLangTag tag = getParentTag(); + if (tag == null) { + return new Object[0]; + } + + XLangAttribute attr = getParentAttr(); + String attrName = attr != null ? attr.getName() : null; + + return XmlPsiHelper.getTagAttrNames(tag) // + .stream() // + .filter(new XLangXdefKeyAttrReference.TagAttrNameFilter(tag)) // + .filter((name) -> !name.equals(attrName)) // + .sorted(XLangReferenceHelper.XLANG_NAME_COMPARATOR) // + .toArray(); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..499963685c2369aef8a55311d7808b30f921675d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReference.java @@ -0,0 +1,21 @@ +/* + * 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.psi.PsiReference; + +/** + * @author flytreeleft + * @date 2025-06-23 + */ +public interface XLangReference extends PsiReference { + + /** {@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 new file mode 100644 index 0000000000000000000000000000000000000000..4722264f67250ca02d390563007ef51151f7c9fc --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceBase.java @@ -0,0 +1,110 @@ +/* + * 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.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; + +/** + * @author flytreeleft + * @date 2025-07-06 + */ +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 unresolvedMessage; + + /** + * 在元素 myElement 可以被拆分为多个相互间有关联的引用时, + * 所构造的各个引用均将从属于 myElement,而 + * myRangeInElement 则为当前引用在 myElement + * 中所对应的文本内容在该元素内所在的相对范围,如,在包 io.nop.core + * 中,nop 包的 {@link TextRange} 为 [3, 6] + *

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

+ * 相关处理逻辑见 {@link com.intellij.psi.impl.SharedPsiElementImplUtil#addReferences SharedPsiElementImplUtil#addReferences} + * + * @param myElement + * 需创建当前引用的元素 + */ + 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; + } + } + + @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; + } + + @Override + public String getUnresolvedMessage() { + return this.unresolvedMessage; + } + + public void setUnresolvedMessage(String unresolvedMessage) { + this.unresolvedMessage = unresolvedMessage; + } + + 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 0000000000000000000000000000000000000000..6e125a39b7215008325b0e6ac89bf31053843f6c --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangReferenceHelper.java @@ -0,0 +1,327 @@ +/* + * 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.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +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.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; +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; + +/** + * @author flytreeleft + * @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(':'); + + 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); + }; + + /** + * 根据{@link XDefTypeDecl 定义类型}识别引用 + * + * @return 若返回 null,则表示未支持对指定类型的处理 + */ + 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 = refDefType.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 -> // + 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, refDefType.getOptions(), refValue) + }; + case STD_DOMAIN_XDEF_ATTR, STD_DOMAIN_DEF_TYPE -> // + getReferencesFromDefType(refElement, refValue, refValue); + default -> new PsiReference[] { + new XLangStdDomainGeneralReference(refElement, textRange, refDefType) + }; + }; + } + + /** 根据属性的类型定义识别引用 */ + public static PsiReference[] getReferencesFromDefType( + 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(); + String defaultValue = Objects.toString(refDefType.getDefaultValue(), null); + List defaultAttrNames = refDefType.getDefaultAttrNames(); + + // Note: 计算引用源文本(XmlAttributeValue#getText 的结果包含引号)与引用值文本之间的文本偏移量, + // 从而精确匹配与引用相关的文本内容 + int textRangeOffset = refElement.getText().indexOf(refValue); + + 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<>(); + + // 引用数据域的类型定义 + TextRange textRange = new TextRange(0, stdDomain.length()).shiftRight(textRangeOffset + stdDomainIndex); + refs.add(new XLangStdDomainReference(refElement, textRange, stdDomain)); + + if (optionsIndex > 0) { + int offset = textRangeOffset + optionsIndex; + textRange = new TextRange(0, options.length()).shiftRight(offset); + + switch (stdDomain) { + case STD_DOMAIN_ENUM -> { + // Note: 忽略 enum:a|b|c|d 形式的数据 + if (StringHelper.isValidClassName(options)) { + refs.add(new XLangStdDomainEnumReference(refElement, textRange, options)); + } + } + case STD_DOMAIN_DICT -> { + refs.add(new XLangStdDomainDictReference(refElement, textRange, options)); + } + } + } + + if (defaultValueIndex > 0) { + textRange = new TextRange(0, defaultValue.length()).shiftRight(textRangeOffset + defaultValueIndex); + + // 引用字典项或枚举值 + switch (stdDomain) { + case STD_DOMAIN_ENUM, STD_DOMAIN_DICT -> { + 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 rangeMap = extractValuesFromCsv(refValue); + + List list = new ArrayList<>(rangeMap.size()); + rangeMap.forEach((textRange, value) -> { + TextRange range = textRange.shiftRight(textRangeOffset); + PsiReference[] refs = getReferencesByVfsPath(refElement, value, range); + + Collections.addAll(list, refs); + }); + + return list.toArray(PsiReference[]::new); + } + + /** 对文本做默认的引用识别 */ + public static PsiReference[] getReferencesFromText(XmlElement refElement, String refValue) { + if (!refValue.endsWith(".xdef") // + && !refValue.endsWith(".xpl") // + && !refValue.endsWith(".xgen") // + && !refValue.endsWith(".xrun") // + && !refValue.endsWith(".xlib") // + ) { + 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) + }; + } + + /** 从 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); + } + + public static NopVirtualFile createNopVfsForDict( + PsiElement refElement, String dictName, Object dictOptionValue + ) { + 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(refElement, 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<>(); + + 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/XLangStdDomainDictReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainDictReference.java new file mode 100644 index 0000000000000000000000000000000000000000..ce36f37fa1d881b99325a0686972d950dba60ec7 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainDictReference.java @@ -0,0 +1,58 @@ +/* + * 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.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.ProjectFileHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对字典的引用 + * + * @author flytreeleft + * @date 2025-07-21 + */ +public class XLangStdDomainDictReference extends XLangReferenceBase { + private final String dictName; + + public XLangStdDomainDictReference(PsiElement myElement, TextRange myRangeInElement, String dictName) { + super(myElement, myRangeInElement); + this.dictName = dictName; + } + + @Override + public @Nullable PsiElement resolveInner() { + NopVirtualFile target = XLangReferenceHelper.createNopVfsForDict(myElement, dictName, null); + + if (target.hasEmptyChildren()) { + String path = target.getPath(); + String msg = NopPluginBundle.message("xlang.annotation.reference.dict-yaml-not-found", path); + setUnresolvedMessage(msg); + + return null; + } + return target; + } + + @Override + public Object @NotNull [] getVariants() { + Project project = myElement.getProject(); + + return ProjectFileHelper.findAllDictNopVfsPaths(project) + .stream() + .map(ProjectFileHelper::getDictNameFromVfsPath) + .sorted(XLangReferenceHelper.XLANG_NAME_COMPARATOR) + .toArray(); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainEnumReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainEnumReference.java new file mode 100644 index 0000000000000000000000000000000000000000..b52ae31a0e17cbe14e3d906ecb2e6966f67afd4a --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainEnumReference.java @@ -0,0 +1,97 @@ +/* + * 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.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.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiPackage; +import com.intellij.psi.search.GlobalSearchScope; +import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.LookupElementHelper; +import io.nop.idea.plugin.utils.PsiClassHelper; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对枚举类的引用 + * + * @author flytreeleft + * @date 2025-07-21 + */ +public class XLangStdDomainEnumReference extends XLangReferenceBase { + private final String enumName; + + public XLangStdDomainEnumReference(PsiElement myElement, TextRange myRangeInElement, String enumName) { + super(myElement, myRangeInElement); + this.enumName = enumName; + } + + @Override + public @Nullable PsiElement resolveInner() { + PsiClass clazz = PsiClassHelper.findClass(myElement, enumName); + + String msg = null; + if (clazz == null) { + msg = NopPluginBundle.message("xlang.annotation.reference.enum-not-found", enumName); + } // + else if (!clazz.isEnum()) { + msg = NopPluginBundle.message("xlang.annotation.reference.class-not-enum", enumName); + } + + if (msg != null) { + setUnresolvedMessage(msg); + return null; + } + return clazz; + } + + @Override + public Object @NotNull [] getVariants() { + Project project = myElement.getProject(); + + String name = StringHelper.removeLastPart(enumName, '.'); + PsiPackage pkg = PsiClassHelper.findPackage(project, name); + if (pkg == null) { + return LookupElement.EMPTY_ARRAY; + } + + GlobalSearchScope scope = PsiClassHelper.getSearchScope(myElement); + + StreamEx pkgStream = StreamEx.of(pkg.getSubPackages(scope)); + StreamEx classStream = StreamEx.of(pkg.getClasses(scope)) + .filter(c -> c.hasModifierProperty(PsiModifier.PUBLIC) && c.isEnum()); + + StreamEx result0 = LookupElementHelper.lookupPsiPackagesStream(pkgStream, this::lookupPackage); + StreamEx result1 = LookupElementHelper.lookupPsiClassesStream(classStream, this::lookupClass); + + return StreamEx.of(result0).append(result1).toArray(); + } + + private LookupElement lookupPackage(PsiPackage pkg) { + // 显示子包名,但插入完整包名 + return LookupElementBuilder.create(pkg.getQualifiedName()) // + .withPresentableText(pkg.getName()) // + .withIcon(pkg.getIcon(Iconable.ICON_FLAG_VISIBILITY)); + } + + private LookupElement lookupClass(PsiClass clazz) { + // 显示类短名字,但插入完整类名 + return LookupElementBuilder.create(clazz.getQualifiedName()) // + .withPresentableText(clazz.getName()) // + .withIcon(clazz.getIcon(Iconable.ICON_FLAG_VISIBILITY)); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGeneralReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGeneralReference.java new file mode 100644 index 0000000000000000000000000000000000000000..e4517dfeff890a69d1d13ebfefd012973086a386 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGeneralReference.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 com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import io.nop.xlang.xdef.XDefTypeDecl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对数据域的通用引用 + *

+ * 主要实现代码补全 + * + * @author flytreeleft + * @date 2025-07-21 + */ +public class XLangStdDomainGeneralReference extends XLangReferenceBase { + private final XDefTypeDecl defType; + + public XLangStdDomainGeneralReference(PsiElement myElement, TextRange myRangeInElement, XDefTypeDecl defType) { + super(myElement, myRangeInElement); + this.defType = defType; + } + + @Override + public boolean isSoft() { + // 标记为"软引用",不触发缺省的错误检查:不影响自定义的 Annotator 检查 + return true; + } + + @Override + public @Nullable PsiElement resolveInner() { + return null; + } + + @Override + public Object @NotNull [] getVariants() { + return switch (defType.getStdDomain()) { + case "boolean" -> new Object[] { Boolean.TRUE.toString(), Boolean.FALSE.toString() }; + default -> new Object[0]; + }; + } +} 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 0000000000000000000000000000000000000000..a32722939606bad87aa2fa9e87b8de00d74c5842 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainGenericTypeReference.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 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; +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; + } + + String className = type instanceof PredefinedPrimitiveType p + ? p.getStdDataType().getJavaClass().getName() + : type.getClassName(); + return PsiClassHelper.findClass(myElement, className); + } + + @Override + public Object @NotNull [] getVariants() { + // TODO generic-type 类型的属性值补全较复杂,暂不处理 + return new Object[0]; + } +} 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 0000000000000000000000000000000000000000..02428bfae7fa77b49c1852782e98b256128470b0 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainReference.java @@ -0,0 +1,109 @@ +/* + * 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.List; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlElement; +import io.nop.api.core.beans.DictBean; +import io.nop.api.core.beans.DictOptionBean; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.LookupElementHelper; +import io.nop.idea.plugin.utils.ProjectFileHelper; +import io.nop.xlang.xdef.IStdDomainHandler; +import io.nop.xlang.xdef.domain.StdDomainRegistry; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 对标准数据域的引用 + * + * @author flytreeleft + * @date 2025-07-15 + */ +public class XLangStdDomainReference extends XLangReferenceBase { + private final String dictName = "core/std-domain"; + private final String stdDomain; + + private DictBean dictBean; + + public XLangStdDomainReference( + XmlElement 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, dictName, stdDomain); + } + + @Override + public Object @NotNull [] getVariants() { + DictOptionBean[] modifiers = new DictOptionBean[] { + new DictOptionBean() {{ + setValue("!"); + setLabel(NopPluginBundle.message("xlang.completion.domain.modifier.required")); + }}, // + new DictOptionBean() {{ + setValue("~"); + setLabel(NopPluginBundle.message("xlang.completion.domain.modifier.internal")); + }}, // + new DictOptionBean() {{ + setValue("#"); + setLabel(NopPluginBundle.message("xlang.completion.domain.modifier.allow-cp-expr")); + }}, // + }; + + String text = myElement.getText(); + return StreamEx.of(modifiers) // + .filter((modifier) -> !text.contains(modifier.getStringValue())) // + .append(XLangReferenceHelper.getRegisteredStdDomains().stream() // + .sorted(XLangReferenceHelper.XLANG_NAME_COMPARATOR) // + .map((value) -> { + DictBean dict = getDict(); + DictOptionBean opt = dict != null + ? dict.getOptionByValue(value) + : null; + + if (opt == null) { + opt = new DictOptionBean(); + opt.setValue(value); + } + return opt; + }) // + ) // + .map(LookupElementHelper::lookupDictOpt) // + .toArray(); + } + + private DictBean getDict() { + if (dictBean == null) { + dictBean = ProjectFileHelper.loadDict(myElement, dictName); + } + return dictBean; + } +} 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 0000000000000000000000000000000000000000..121c6a91f85b78edef1da51871847848ecfd83eb --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangStdDomainXdefRefReference.java @@ -0,0 +1,160 @@ +/* + * 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.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.PsiNamedElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import io.nop.idea.plugin.lang.psi.XLangTag; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.ProjectFileHelper; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; +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; + } + + private XLangTag getParentTag() { + return PsiTreeUtil.getParentOfType(myElement, XLangTag.class); + } + + @Override + public @Nullable PsiElement resolveInner() { + // - /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(myElement, 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); + setUnresolvedMessage(msg); + } + // 不能引用自身 + else if (target == getXdefNameAttr(getParentTag())) { + String msg = NopPluginBundle.message("xlang.annotation.reference.x-prototype-attr-self-referenced", + ((PsiNamedElement) target).getName(), + attrValue); + setUnresolvedMessage(msg); + + return null; + } + + return target; + } + + @Override + public Object @NotNull [] getVariants() { + // Note: 在自动补全阶段,DSL 结构很可能是不完整的,只能从 xml 角度做分析 + Project project = myElement.getProject(); + + List names = new ArrayList<>(); + PsiTreeUtil.processElements(myElement.getContainingFile(), element -> { + if (element instanceof XmlAttribute attr) { + String name = attr.getName(); + String value = attr.getValue(); + + // Note: xdef-ref 引用的只能是 xdef:name 命名的节点 + if ("xdef:name".equals(name) || "meta:name".equals(name)) { + names.add(value); + } + } + return true; + }); + + return StreamEx.of( // + names.stream().sorted(XLangReferenceHelper.XLANG_NAME_COMPARATOR) // + ) // + .append(ProjectFileHelper.findAllXdefNopVfsPaths(project) + .stream() + .sorted(XLangReferenceHelper.XLANG_NAME_COMPARATOR) // + ) // + .toArray(); + } + + private XmlAttribute getXdefNameAttr(XLangTag tag) { + if (tag == null) { + return null; + } + + XmlAttribute attr = tag.getAttribute("xdef:name"); + + if (attr == null) { + attr = tag.getAttribute("meta:name"); + } + return attr; + } + + 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/XLangTagReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java new file mode 100644 index 0000000000000000000000000000000000000000..0f6b20aed3c60a74229882951bfad2359a047533 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangTagReference.java @@ -0,0 +1,134 @@ +/* + * 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.lookup.LookupElement; +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.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; +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 XLangTagReference(XLangTag myElement, TextRange myRangeInElement) { + super(myElement, myRangeInElement); + } + + @Override + public @Nullable PsiElement resolveInner() { + XLangTag tag = (XLangTag) myElement; + IXDefNode defNode = tag.getSchemaDefNode(); + + String path = XmlPsiHelper.getNopVfsPath(defNode); + if (path == null) { + return null; + } + + SourceLocation loc = defNode.getLocation(); + Function targetResolver = (file) -> XmlPsiHelper.getPsiElementAt(file, + loc, + XLangTag.class); + + return new NopVirtualFile(myElement, path, targetResolver); + } + + @Override + public Object @NotNull [] getVariants() { + // Note: 在自动补全阶段,DSL 结构很可能是不完整的,只能从 xml 角度做分析 + 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) -> XLangReferenceHelper.XLANG_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 LookupElementHelper.lookupXmlTag(tagName, label); + } +} 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 0000000000000000000000000000000000000000..4f749e60ef5da4e775dd5002066f8cf8a1ca8c47 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXPrototypeReference.java @@ -0,0 +1,130 @@ +/* + * 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.Arrays; +import java.util.Objects; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.util.PsiTreeUtil; +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 io.nop.idea.plugin.utils.XmlPsiHelper; +import io.nop.xlang.xdef.IXDefNode; +import org.jetbrains.annotations.NotNull; +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; + } + + private XLangTag getParentTag() { + return PsiTreeUtil.getParentOfType(myElement, XLangTag.class); + } + + @Override + public @Nullable PsiElement resolveInner() { + XLangTag tag = getParentTag(); + if (tag == null) { + return null; + } + + XLangTag parentTag = tag.getParentTag(); + if (parentTag == null) { + String msg = NopPluginBundle.message("xlang.annotation.reference.x-prototype-no-parent"); + setUnresolvedMessage(msg); + + return null; + } + + String keyAttr = getKeyAttrName(tag, parentTag); + + 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); + setUnresolvedMessage(msg); + + return null; + } + // 不能引用自身 + else if (protoTag == tag) { + String msg = keyAttr == null + ? NopPluginBundle.message("xlang.annotation.reference.x-prototype-tag-self-referenced", + attrValue) + : NopPluginBundle.message("xlang.annotation.reference.x-prototype-attr-self-referenced", + keyAttr, + attrValue); + setUnresolvedMessage(msg); + + return null; + } + + // 定位到目标属性或标签上 + return getKeyAttrElement(protoTag, keyAttr); + } + + @Override + public Object @NotNull [] getVariants() { + // Note: 在自动补全阶段,DSL 结构很可能是不完整的,只能从 xml 角度做分析 + XLangTag tag = getParentTag(); + XLangTag parentTag = tag != null ? tag.getParentTag() : null; + if (parentTag == null) { + return new Object[0]; + } + + String keyAttr = getKeyAttrName(tag, parentTag); + + return Arrays.stream(parentTag.getChildren()) + .filter((child) -> child != tag && child instanceof XLangTag) + .map((child) -> (XLangTag) child) + .map((child) -> getKeyAttrElement(child, keyAttr)) + .filter(Objects::nonNull) + .map((child) -> child instanceof XLangAttribute attr ? attr.getValue() : child.getName()) + .filter(Objects::nonNull) + .sorted(XLangReferenceHelper.XLANG_NAME_COMPARATOR) + .toArray(); + } + + private String getKeyAttrName(XLangTag tag, XLangTag parentTag) { + // 仅从父节点中取引用到的子节点 + // io.nop.xlang.delta.DeltaMerger#mergePrototype + IXDefNode defNode = tag.getSchemaDefNode(); + IXDefNode parentDefNode = parentTag.getSchemaDefNode(); + + String keyAttr = parentDefNode.getXdefKeyAttr(); + + if (keyAttr == null) { + keyAttr = defNode.getXdefUniqueAttr(); + } + return keyAttr; + } + + private PsiNamedElement getKeyAttrElement(XLangTag tag, String keyAttr) { + return keyAttr != null ? tag.getAttribute(keyAttr) : tag; + } +} 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 0000000000000000000000000000000000000000..8294506e14e1f49dfb178663fd5a4449e0da1284 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefKeyAttrReference.java @@ -0,0 +1,103 @@ +/* + * 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.Predicate; + +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.commons.util.StringHelper; +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.XDefKeys; +import io.nop.xlang.xdsl.XDslKeys; +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; + } + + private XLangTag getParentTag() { + return PsiTreeUtil.getParentOfType(myElement, XLangTag.class); + } + + @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); + setUnresolvedMessage(msg); + } + + return results.length == 1 ? results[0].getElement() : null; + } + + @Override + public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { + XLangTag tag = getParentTag(); + if (tag == null) { + return ResolveResult.EMPTY_ARRAY; + } + + return XmlPsiHelper.getAttrsFromChildTag(tag, attrValue).stream() // + .map(PsiElementResolveResult::new) // + .toArray(ResolveResult[]::new); + } + + @Override + public Object @NotNull [] getVariants() { + // Note: 在自动补全阶段,DSL 结构很可能是不完整的,只能从 xml 角度做分析 + XLangTag tag = getParentTag(); + if (tag == null) { + return new Object[0]; + } + + return XmlPsiHelper.getCommonAttrNamesFromChildTag(tag) // + .stream() // + .filter(new TagAttrNameFilter(tag)) // + .sorted(XLangReferenceHelper.XLANG_NAME_COMPARATOR) // + .toArray(); + } + + static class TagAttrNameFilter implements Predicate { + private final XLangTag refTag; + + TagAttrNameFilter(XLangTag refTag) { + this.refTag = refTag; + } + + @Override + public boolean test(String name) { + XDefKeys xdefKeys = refTag.getXDefKeys(); + XDslKeys xdslKeys = refTag.getXDslKeys(); + + String ns = StringHelper.getNamespace(name); + + return !xdefKeys.NS.equals(ns) && !xdslKeys.NS.equals(ns); + } + } +} 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 0000000000000000000000000000000000000000..7992ec67e9edc7bd8f19f8df3fce3bb91e7dbcce --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXdefNameReference.java @@ -0,0 +1,52 @@ +/* + * 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.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/lang/reference/XLangXlibTagAttrReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagAttrReference.java new file mode 100644 index 0000000000000000000000000000000000000000..2f5826f6efea02f9c044e4802cb6e5af9aa60043 --- /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/XLangXlibTagNsReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagNsReference.java new file mode 100644 index 0000000000000000000000000000000000000000..53857c523e89e546bd372c384794bd2a9228c90c --- /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 0000000000000000000000000000000000000000..57ef83882d78d297bf15fdbce7f3e09b0aeba1a1 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/reference/XLangXlibTagReference.java @@ -0,0 +1,96 @@ +/* + * 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.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; +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 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()); + } + + 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); + + 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() { + return XlibTagMeta.withLoadedXlib(myElement, xlibPath, (xlib) -> { + // Note: 标签函数允许递归引用,不需要排除当前标签 + return xlib.getTags().keySet().stream() // + .sorted() // + .map((name) -> LookupElementHelper.lookupXmlTag(name, null)) // + .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/lang/script/XLangScriptASTFactory.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e9bf82635ab42e718a5c0116807c1dfbb0036cee --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptASTFactory.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.script; + +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.Identifier; +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 + */ +public class XLangScriptASTFactory extends ASTFactory { + + // Note: 封装 CompositeElement,直接在 XLangScriptParserDefinition#createElement + // 中创建 PsiElement,避免非必要对象的定义和创建 + + /** 为 AST 树的叶子节点创建 {@link com.intellij.psi.PsiElement PsiElement} */ + @Override + public @Nullable LeafElement createLeaf(@NotNull IElementType token, @NotNull CharSequence text) { + if (token == TOKEN_Identifier) { + return new Identifier(token, text); + } + + return new LeafPsiElement(token, text); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptFile.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptFile.java new file mode 100644 index 0000000000000000000000000000000000000000..4d3928ab28862e5c6e0b61a2a17e68092f9669ac --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptFile.java @@ -0,0 +1,30 @@ +/* + * 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.script; + +import com.intellij.extapi.psi.PsiFileBase; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.psi.FileViewProvider; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-28 + */ +public class XLangScriptFile extends PsiFileBase { + + protected XLangScriptFile(@NotNull FileViewProvider viewProvider) { + super(viewProvider, XLangScriptLanguage.INSTANCE); + } + + @Override + public @NotNull FileType getFileType() { + return XLangScriptFileType.INSTANCE; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..8753a69a8ab2d89232e78bfadcd9fd5efb454e40 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptFileType.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.script; + +import javax.swing.*; + +import com.intellij.openapi.fileTypes.LanguageFileType; +import io.nop.idea.plugin.icons.NopIcons; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 注意,必须在 plugin.xml 中注册 <fileType/> 后才能在 + * {@link io.nop.idea.plugin.lang.XLangScriptLanguageInjector XLangScriptLanguageInjector} + * 中识别 XLang Script 片段 + * + * @author flytreeleft + * @date 2025-06-28 + */ +public class XLangScriptFileType extends LanguageFileType { + public static final XLangScriptFileType INSTANCE = new XLangScriptFileType(); + + private XLangScriptFileType() { + super(XLangScriptLanguage.INSTANCE); + } + + @NotNull + @Override + public String getName() { + return "XLang Script"; + } + + @NotNull + @Override + public String getDescription() { + return "XLang Script (Embedded in XLang DSL)"; + } + + @NotNull + @Override + public String getDefaultExtension() { + // Note: 预期不会用于检测文件,故而,通过长名字以避免与其他文件类型发生冲突 + return "xlangscript"; + } + + @Nullable + @Override + public Icon getIcon() { + return NopIcons.XLangFileType; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..da01962f898280cb96962b9c5702a0bd2f0e0e48 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptLanguage.java @@ -0,0 +1,23 @@ +/* + * 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.script; + +import com.intellij.lang.Language; + +/** + * @author flytreeleft + * @date 2025-06-27 + */ +public class XLangScriptLanguage extends Language { + public static final XLangScriptLanguage INSTANCE = new XLangScriptLanguage(); + + private XLangScriptLanguage() { + super((Language) null, "XLangScript"); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptLexerAdaptor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptLexerAdaptor.java new file mode 100644 index 0000000000000000000000000000000000000000..1a8870e0f7f89a951a90fc9fdb34c00529a1da46 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptLexerAdaptor.java @@ -0,0 +1,23 @@ +/* + * 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.script; + +import io.nop.xlang.parse.antlr.XLangLexer; +import org.antlr.intellij.adaptor.lexer.ANTLRLexerAdaptor; + +/** + * @author flytreeleft + * @date 2025-06-27 + */ +public class XLangScriptLexerAdaptor extends ANTLRLexerAdaptor { + + public XLangScriptLexerAdaptor() { + super(XLangScriptLanguage.INSTANCE, new XLangLexer(null)); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeManipulator.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeManipulator.java new file mode 100644 index 0000000000000000000000000000000000000000..d293b0710371758a738db0c5d511a80a3843733b --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeManipulator.java @@ -0,0 +1,83 @@ +/* + * 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.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 XLangScriptNodeManipulator extends AbstractElementManipulator { + + /** 对 element 采取就地更新策略 */ + @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); + + QualifiedNameNode qn = findQualifiedNameNode(node); + imp.getQualifiedName().replace(qn); + + 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; + } + + protected QualifiedNameNode findQualifiedNameNode(PsiElement node) { + while (node != null) { + if (node instanceof QualifiedNameNode qn) { + return qn; + } + + if (node instanceof ImportDeclarationNode) { + // 跳过 import 和 空白 + node = node.getFirstChild().getNextSibling().getNextSibling(); + } else { + node = node.getFirstChild(); + } + } + return null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeRenameProcessor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeRenameProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..665a6bfc69bd75877033d159dd4ba6202128bd43 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptNodeRenameProcessor.java @@ -0,0 +1,76 @@ +/* + * 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.script; + +import java.util.Map; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.search.SearchScope; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.rename.RenamePsiElementProcessor; +import io.nop.idea.plugin.lang.script.psi.IdentifierNode; +import io.nop.idea.plugin.lang.script.psi.ProgramNode; +import io.nop.idea.plugin.lang.script.reference.IdentifierReference; +import org.jetbrains.annotations.NotNull; + +/** + * XLang Script 的本地变量重命名处理器 + *

+ * 默认的重命名处理器 {@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/XLangScriptParserAdaptor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java new file mode 100644 index 0000000000000000000000000000000000000000..dcc40cd750e40b4e5c8d0c217485643cd27278db --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserAdaptor.java @@ -0,0 +1,37 @@ +/* + * 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.script; + +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.tree.ParseTree; + +/** + * @author flytreeleft + * @date 2025-06-27 + */ +public class XLangScriptParserAdaptor extends ANTLRParserAdaptor { + + public XLangScriptParserAdaptor() { + super(XLangScriptLanguage.INSTANCE, new XLangParser(null)); + } + + @Override + protected ParseTree parse(Parser parser, IElementType root) { + // Note: 不需要为 dot 节点之后的空白添加占位节点,只要延迟到在 + // PsiReference#resolve 中才获取引用元素,即可正常触发代码补全 + if (root instanceof IFileElementType) { + return ((XLangParser) parser).program(); + } + 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 new file mode 100644 index 0000000000000000000000000000000000000000..ac71bd7f150b68e9aadef8e37cdd7dd0dc4646b0 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptParserDefinition.java @@ -0,0 +1,196 @@ +/* + * 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.script; + +import com.intellij.lang.ASTNode; +import com.intellij.lang.ParserDefinition; +import com.intellij.lang.PsiParser; +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.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; +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; +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.ObjectPropertyDeclarationNode; +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; + +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 + * + * @author flytreeleft + * @date 2025-06-27 + */ +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) { + return new XLangScriptLexerAdaptor(); + } + + @NotNull + @Override + public PsiParser createParser(Project project) { + return new XLangScriptParserAdaptor(); + } + + /** Note: 只有在 {@link com.intellij.lang.ASTFactory ASTFactory} 中未创建 PsiElement 的节点才会调用该接口 */ + @NotNull + @Override + public PsiElement createElement(ASTNode node) { + if (!(node.getElementType() instanceof RuleIElementType rule)) { + return new RuleSpecNode(node); + } + + return switch (rule.getRuleIndex()) { + case XLangParser.RULE_program -> // + new ProgramNode(node); + case XLangParser.RULE_ast_topLevelStatement -> // + new TopLevelStatementNode(node); + case XLangParser.RULE_statement -> // + new StatementNode(node); + case XLangParser.RULE_identifier -> // + new IdentifierNode(node); + case XLangParser.RULE_literal -> // + new LiteralNode(node); + // + case XLangParser.RULE_importAsDeclaration -> // + 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); + case XLangParser.RULE_variableDeclarators_ -> // + new VariableDeclaratorsNode(node); + case XLangParser.RULE_variableDeclarator -> // + 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_typeNameNode_predefined -> // + new TypeNameNodePredefinedNode(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_qualifiedName_ -> // + new QualifiedNameRootNode(node); + case XLangParser.RULE_arguments_ -> // + new CalleeArgumentsNode(node); + case XLangParser.RULE_identifier_ex -> // + new ObjectMemberNode(node); + case XLangParser.RULE_propertyAssignment -> // + new ObjectPropertyAssignmentNode(node); + // + 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 -> // + new ArrowFunctionNode(node); + case XLangParser.RULE_expression_functionBody -> // + new ArrowFunctionBodyNode(node); + case XLangParser.RULE_returnStatement -> // + new ReturnStatementNode(node); + default -> new RuleSpecNode(node); + }; + } + + @NotNull + @Override + public PsiFile createFile(@NotNull FileViewProvider viewProvider) { + return new XLangScriptFile(viewProvider); + } + + @NotNull + @Override + public IFileElementType getFileNodeType() { + return FILE; + } + + @NotNull + @Override + public SpaceRequirements spaceExistenceTypeBetweenTokens(ASTNode left, ASTNode right) { + return SpaceRequirements.MAY; + } + + @NotNull + @Override + public TokenSet getWhitespaceTokens() { + return TOKEN_whitespace; + } + + @NotNull + @Override + public TokenSet getCommentTokens() { + return TOKEN_comment; + } + + @NotNull + @Override + public TokenSet getStringLiteralElements() { + return TOKEN_literal_string; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..514d1665a95843f499e9869643eb92d6cc1eaf4e --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptSyntaxHighlighter.java @@ -0,0 +1,103 @@ +/* + * 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.script; + +import com.intellij.ide.highlighter.JavaHighlightingColors; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.fileTypes.SyntaxHighlighterBase; +import com.intellij.psi.tree.IElementType; +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.TokenIElementType; +import org.jetbrains.annotations.NotNull; + +/** + * 参考:https://github.com/antlr/jetbrains-plugin-sample/blob/master/src/main/java/org/antlr/jetbrains/sample/SampleSyntaxHighlighter.java + * + * @author flytreeleft + * @date 2025-06-27 + */ +public class XLangScriptSyntaxHighlighter extends SyntaxHighlighterBase { + + static { + PSIElementTypeFactory.defineLanguageIElementTypes(XLangScriptLanguage.INSTANCE, + XLangLexer.tokenNames, + XLangParser.ruleNames); + } + + @NotNull + @Override + public Lexer getHighlightingLexer() { + return new XLangScriptLexerAdaptor(); + } + + @Override + public TextAttributesKey @NotNull [] getTokenHighlights(IElementType tokenType) { + if (!(tokenType instanceof TokenIElementType myType)) { + return TextAttributesKey.EMPTY_ARRAY; + } + + TextAttributesKey attrKey = switch (myType.getANTLRTokenType()) { + case XLangLexer.Identifier -> // + DefaultLanguageHighlighterColors.IDENTIFIER; + case XLangLexer.StringLiteral, XLangLexer.TemplateStringLiteral -> // + JavaHighlightingColors.STRING; + case XLangLexer.DecimalIntegerLiteral, XLangLexer.HexIntegerLiteral, // + XLangLexer.BinaryIntegerLiteral, XLangLexer.DecimalLiteral -> // + JavaHighlightingColors.NUMBER; + case XLangLexer.OpenBracket, XLangLexer.CloseBracket, XLangLexer.CpExprStart -> // + JavaHighlightingColors.BRACKETS; + case XLangLexer.OpenBrace, XLangLexer.CloseBrace -> // + JavaHighlightingColors.BRACES; + case XLangLexer.OpenParen, XLangLexer.CloseParen -> // + JavaHighlightingColors.PARENTHESES; + case XLangLexer.SemiColon -> // + JavaHighlightingColors.JAVA_SEMICOLON; + case XLangLexer.Comma -> // + JavaHighlightingColors.COMMA; + case XLangLexer.Dot -> // + JavaHighlightingColors.DOT; + case XLangLexer.UnexpectedCharacter -> // + JavaHighlightingColors.INVALID_STRING_ESCAPE; + case XLangLexer.SingleLineComment -> // + 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; + }; + + return attrKey != null ? new TextAttributesKey[] { attrKey } : TextAttributesKey.EMPTY_ARRAY; + } +} 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 0000000000000000000000000000000000000000..502e8793b39e0f919f90455741a2aeb14d02559a --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/XLangScriptTokenTypes.java @@ -0,0 +1,66 @@ +/* + * 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.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 { + // Note: 需在 XLangScriptParserDefinition 中完成对 PSIElementTypeFactory 的初始化 + 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 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); + } + + 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/psi/ArrayExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrayExpressionNode.java new file mode 100644 index 0000000000000000000000000000000000000000..137d775be5e91269a654d308bc89b6ade1d54128 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrayExpressionNode.java @@ -0,0 +1,54 @@ +/* + * 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.script.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import org.jetbrains.annotations.NotNull; + +/** + * [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/ArrowFunctionBodyNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionBodyNode.java new file mode 100644 index 0000000000000000000000000000000000000000..cfb78195923dc62ac9f94491be015916a4524f9d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionBodyNode.java @@ -0,0 +1,23 @@ +/* + * 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.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-30 + */ +public class ArrowFunctionBodyNode extends RuleSpecNode { + + 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 0000000000000000000000000000000000000000..59c4cb6432cf656d19277a06395e75978623a757 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java @@ -0,0 +1,64 @@ +/* + * 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.script.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +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 + */ +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/AssignmentExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java new file mode 100644 index 0000000000000000000000000000000000000000..3a150ec04c5937c0fefa7078bf788121d0ff5811 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java @@ -0,0 +1,81 @@ +/* + * 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.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/BlockStatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java new file mode 100644 index 0000000000000000000000000000000000000000..4d253fb2b265d264f099ae9b265ee06d72640633 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java @@ -0,0 +1,27 @@ +/* + * 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.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 0000000000000000000000000000000000000000..080fbaa1fe7b217d2617402994f9c9050fa7f912 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java @@ -0,0 +1,55 @@ +/* + * 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.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 + */ +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 new file mode 100644 index 0000000000000000000000000000000000000000..673f57d9fccc11497d377b9c32ee128f5a92c6f3 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java @@ -0,0 +1,573 @@ +/* + * 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.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; +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 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 org.jetbrains.annotations.NotNull; + +import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_parameterizedTypeNode; + +/** + * 表达式节点 + *

+ * {@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
+ * 
+ *

+ * 对象方法调用 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 { + + public ExpressionNode(@NotNull ASTNode node) { + super(node); + } + + @Override + protected PsiReference @NotNull [] doGetReferences() { + // Note: + // - 仅识别当前表达式的最后一个有效元素的引用,其余部分,由其子表达式做识别处理 + // - 对象声明节点 ObjectDeclarationNode 的相关引用,由其自身负责构造 + // - 对构造函数中的 QualifiedNameRootNode 的相关引用,由其自身负责构造 + + PsiElement firstChild = getFirstChild(); + // 变量引用:abc + if (firstChild instanceof IdentifierNode identifier) { + TextRange textRange = identifier.getTextRangeInParent(); + IdentifierReference ref = new IdentifierReference(this, textRange, identifier); + + return new PsiReference[] { ref }; + } + // 对象方法调用:a.b.c(1, 2) + else if (isObjectMethodCall()) { + ObjectMethodReference ref = new ObjectMethodReference(this); + + return new PsiReference[] { ref }; + } + // 对象属性访问:a.b.c + else if (isObjectMemberAccess()) { + ObjectMemberReference ref = new ObjectMemberReference(this); + + return new PsiReference[] { ref }; + } + // 函数调用:fn1(1, 2, 3) + else if (isFunctionCall()) { + ExpressionNode callee = (ExpressionNode) firstChild; + + TextRange textRange = callee.getTextRangeInParent(); + IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); + + IdentifierReference ref = new IdentifierReference(this, textRange, fn); + + return new PsiReference[] { ref }; + } + + return PsiReference.EMPTY_ARRAY; + } + + /** + * 获取表达式结果的类型 + * + * @return null 为有效值 + */ + public PsiClass getResultType() { + PsiElement firstChild = getFirstChild(); + + if (isObjectConstructorCall()) { + RuleSpecNode ptn = findChildByType(RULE_parameterizedTypeNode); + QualifiedNameRootNode cons = ptn != null ? (QualifiedNameRootNode) ptn.getFirstChild() : null; + + return cons != null ? cons.getQualifiedType() : null; + } // + else if (isObjectMethodCall()) { + PsiMethod method = getObjectMethod(); + PsiType returnType = method != null ? method.getReturnType() : null; + + return getPsiClassByPsiType(returnType); + } // + else if (isObjectMemberAccess()) { + PsiElement member = getObjectMember(); + if (!(member instanceof PsiField prop)) { + return null; + } + + PsiType propType = prop.getType(); + + return getPsiClassByPsiType(propType); + } // + else if (isFunctionCall()) { + ExpressionNode callee = (ExpressionNode) firstChild; + IdentifierNode fn = (IdentifierNode) callee.getFirstChild(); + + // Note: 对应的是函数的返回值类型 + return fn.getVarType(); + } // + else if (isArrowFunction()) { + ArrowFunctionNode fn = (ArrowFunctionNode) firstChild; + + return fn.getReturnType(); + } // + else if (isArrayInit()) { + ArrayExpressionNode array = (ArrayExpressionNode) firstChild; + + 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; + } + + /** + * 当前表达式是否为对象成员(属性或方法)访问 + *

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

+ * 在父节点未包含 {@link CalleeArgumentsNode} 节点时, + * 当前对象成员可能是变量,也可能是方法 + */ + public boolean isObjectMemberAccess() { + // a.b.c + if (getFirstChild() instanceof ExpressionNode) { + return getLastChild() instanceof ObjectMemberNode // + && !(getParent().getLastChild() instanceof CalleeArgumentsNode); + } + return false; + } + + /** + * 当前表达式是否为对象方法调用 + *

+ * 从最后一个对象成员的视角向上观察 + */ + public boolean isObjectMethodCall() { + // a.b.c() + if (getFirstChild() instanceof ExpressionNode) { + return getLastChild() instanceof ObjectMemberNode // + && getParent().getLastChild() instanceof CalleeArgumentsNode; + } + return false; + } + + /** 当前表达式是否为对象构造函数调用 */ + public boolean isObjectConstructorCall() { + // new String("abc") + return getFirstChild().getText().equals("new") && getLastChild() instanceof CalleeArgumentsNode; + } + + /** 当前表达式是否为函数调用 */ + public boolean isFunctionCall() { + // a(1) + if (getFirstChild() instanceof ExpressionNode callee) { + 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; + } + + /** 获取对象的 class */ + public PsiClass getObjectClass() { + ExpressionNode obj = (ExpressionNode) getFirstChild(); + + return obj.getResultType(); + } + + /** 获取对象的方法 */ + public PsiMethod getObjectMethod() { + PsiMethod[] methods = getObjectMethods(); + + return filterMethodByArgs(methods, () -> ((ExpressionNode) getParent()).getObjectMethodArgumentTypes()); + } + + /** 获取对象的成员(属性或方法) */ + 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} */ + public TextRange getObjectMemberTextRange() { + ObjectMemberNode member = (ObjectMemberNode) getLastChild(); + + return member.getTextRangeInParent(); + } + + /** 获取对象的方法 */ + protected PsiMethod @NotNull [] getObjectMethods() { + return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findMethodsByName(objClass, memberName, true), + PsiMethod.EMPTY_ARRAY); + } + + /** 获取调用参数的类型列表 */ + protected PsiClass[] getObjectMethodArgumentTypes() { + CalleeArgumentsNode calleeArgs = (CalleeArgumentsNode) getLastChild(); + + return calleeArgs.getArgumentTypes(); + } + + protected T getObjectMember(BiFunction consumer, T defaultValue) { + PsiClass objClass = getObjectClass(); + if (objClass == null) { + return defaultValue; + } + + ObjectMemberNode member = (ObjectMemberNode) getLastChild(); + String memberName = member.getText(); + + return consumer.apply(objClass, memberName); + } + + 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(); + + if (matchMethodParams(params, args)) { + return method; + } + } + + // 再查找参数数量一致的方法 + for (PsiMethod method : methods) { + if (method.getParameterList().getParametersCount() == args.length) { + return method; + } + } + + // 若都不匹配,则取第一个 + return methods.length > 0 ? methods[0] : null; + } + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..ac684f68b78972bdce0af01ba7b458c559bd90b9 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java @@ -0,0 +1,82 @@ +/* + * 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.script.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import org.jetbrains.annotations.NotNull; + +/** + * 函数声明节点 + *

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

+ * FunctionDeclarationNode(functionDeclaration)
+ *   PsiElement('function')('function')
+ *   PsiWhiteSpace(' ')
+ *   IdentifierNode(identifier)
+ *     PsiElement(Identifier)('fn')
+ *   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(' ')
+ *   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 { + + public FunctionDeclarationNode(@NotNull ASTNode node) { + super(node); + } + + public IdentifierNode getFunctionNameNode() { + return findChildByClass(IdentifierNode.class); + } + + /** 获取函数的返回值类型 */ + public PsiClass getReturnType() { + // TODO 分析函数的 return 表达式,得到返回类型 + return null; + } +} 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 0000000000000000000000000000000000000000..2b536630e895e69290210e1579fd34f064a5525a --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java @@ -0,0 +1,69 @@ +/* + * 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.script.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +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 { + + public FunctionParameterDeclarationNode(@NotNull ASTNode node) { + super(node); + } + + public IdentifierNode getParameterName() { + return (IdentifierNode) getFirstChild().getFirstChild(); + } + + public PsiClass getParameterType() { + RuleSpecNode typeNode = findChildByType(RULE_namedTypeNode_annotation); + if (typeNode == null) { + return null; + } + + RuleSpecNode type = (RuleSpecNode) typeNode.getLastChild().getFirstChild(); + + 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/FunctionParameterListNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java new file mode 100644 index 0000000000000000000000000000000000000000..cb4837ec0de0ceee687a2e9f808dee0c3043700f --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java @@ -0,0 +1,89 @@ +/* + * 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.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/Identifier.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java new file mode 100644 index 0000000000000000000000000000000000000000..6ba36be011cc2f62140e98f498de92ef5f7b4c66 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java @@ -0,0 +1,28 @@ +/* + * 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.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 new file mode 100644 index 0000000000000000000000000000000000000000..0cab09759e8d321907c79a234b026dd6cb3ac280 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java @@ -0,0 +1,83 @@ +/* + * 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.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; +import org.jetbrains.annotations.NotNull; + +/** + * {@link Identifier} 节点 + *

+ * 该节点 AST 树为: + *

+ * IdentifierNode(identifier)
+ *   PsiElement(Identifier)('b')
+ * 
+ * + * @author flytreeleft + * @date 2025-07-02 + */ +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(); + + return varDecl != null ? varDecl.type() : null; + } + + /** 获取变量的定义信息 */ + public XLangVarDecl getVarDecl() { + String varName = getText(); + XLangVarDecl decl = findVisibleVar(varName); + + if (decl == null // + && varName.indexOf('.') < 0 // + && varName.indexOf('$') < 0 // + && StringHelper.isValidClassName(varName) // + ) { + // Note: java.lang 中的类不需要显式导入 + PsiClass clazz = PsiClassHelper.findClass(this, "java.lang." + varName); + if (clazz != null) { + decl = new XLangVarDecl(clazz, clazz); + } + } + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..69fad2b832300cfc3ce182d81e97263f15b03bd7 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java @@ -0,0 +1,72 @@ +/* + * 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.script.psi; + +import java.util.Map; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import io.nop.idea.plugin.lang.XLangVarDecl; +import io.nop.idea.plugin.utils.PsiClassHelper; +import org.jetbrains.annotations.NotNull; + +/** + * 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 { + + public ImportDeclarationNode(@NotNull ASTNode node) { + super(node); + } + + @Override + public @NotNull Map getVars() { + ImportSourceNode imp = findChildByClass(ImportSourceNode.class); + + QualifiedNameNode qualifiedName = imp != null ? imp.getQualifiedName() : null; + if (qualifiedName == null) { + return Map.of(); + } + + String classFQN = qualifiedName.getFullyName(); + PsiClass clazz = PsiClassHelper.findClass(this, classFQN); + if (clazz == null) { + return Map.of(); + } + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..86b28dbc79b4eb3fe9420ec8d2a2e3c82f62ab71 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java @@ -0,0 +1,59 @@ +/* + * 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.script.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiReference; +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 { + + public ImportSourceNode(@NotNull ASTNode node) { + super(node); + } + + public QualifiedNameNode getQualifiedName() { + return (QualifiedNameNode) getFirstChild(); + } + + /** 构造 Java 相关的引用对象,从而支持自动补全、引用跳转、文档显示等 */ + @Override + protected PsiReference @NotNull [] doGetReferences() { + QualifiedNameNode qnn = getQualifiedName(); + String fqn = qnn.getFullyName(); + + return PsiClassHelper.createJavaClassReferences(this, fqn, 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 new file mode 100644 index 0000000000000000000000000000000000000000..539344a3db03dc3c80841c07a925d5265ff7986c --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java @@ -0,0 +1,52 @@ +/* + * 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.script.psi; + +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 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; + + public LiteralNode(@NotNull ASTNode node) { + super(node); + } + + public LeafPsiElement getLiteral() { + if (literal == null || !literal.isValid()) { + literal = (LeafPsiElement) PsiTreeUtil.getDeepestLast(this); + } + return literal; + } + + /** 获取字面量的数据类型 */ + public PsiClass getDataType() { + LeafPsiElement target = getLiteral(); + TokenIElementType token = (TokenIElementType) target.getElementType(); + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..8173822583ac304749f6fd4937175a2a7ff8d8f8 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java @@ -0,0 +1,92 @@ +/* + * 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.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.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; + +/** + * 对象声明节点 + *

+ * {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 + * @date 2025-07-02 + */ +public class ObjectDeclarationNode extends RuleSpecNode { + + public ObjectDeclarationNode(@NotNull ASTNode node) { + super(node); + } + + @Override + protected 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(); + + IdentifierReference ref = new IdentifierReference(this, textRange, propNameNode); + + 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/ObjectMemberNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectMemberNode.java new file mode 100644 index 0000000000000000000000000000000000000000..e3c32f1037e8647c702296cb968057f17eb738fe --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectMemberNode.java @@ -0,0 +1,28 @@ +/* + * 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.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 ObjectMemberNode extends RuleSpecNode { + + public ObjectMemberNode(@NotNull ASTNode node) { + super(node); + } +} 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 0000000000000000000000000000000000000000..47383a40fbaaa2189ad99e822aec26cd53fbbf49 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java @@ -0,0 +1,70 @@ +/* + * 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.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 + */ +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/ObjectPropertyDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java new file mode 100644 index 0000000000000000000000000000000000000000..e445293dff5e8cfee02ddaa41078e8e74456a3c8 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java @@ -0,0 +1,28 @@ +/* + * 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.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/ProgramNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java new file mode 100644 index 0000000000000000000000000000000000000000..e801cde2be08bddb0ee78d96953dcdf3397cc168 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java @@ -0,0 +1,28 @@ +/* + * 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.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); + } +} 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 0000000000000000000000000000000000000000..1e375adfc8525416b1122d9d2ce122395cb0aa08 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.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.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 { + + public QualifiedNameNode(@NotNull ASTNode node) { + super(node); + } + + public IdentifierNode getIdentifier() { + return (IdentifierNode) getFirstChild().getFirstChild(); + } + + 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/QualifiedNameRootNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java new file mode 100644 index 0000000000000000000000000000000000000000..992e3299fcf26a4898861f4486ce7cd5462a0751 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java @@ -0,0 +1,117 @@ +/* + * 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.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.QualifiedNameReference; +import org.jetbrains.annotations.NotNull; + +/** + * 类名节点 + *

+ * String: + *

+ * QualifiedNameRootNode(qualifiedName_)
+ *   QualifiedNameNode(qualifiedName)
+ *     RuleSpecNode(qualifiedName_name_)
+ *       IdentifierNode(identifier)
+ *         PsiElement(Identifier)('String')
+ * 
+ * + * Abc.Def: + *
+ * QualifiedNameRootNode(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 QualifiedNameRootNode extends RuleSpecNode { + + public QualifiedNameRootNode(@NotNull ASTNode node) { + super(node); + } + + public QualifiedNameNode getQualifiedName() { + return (QualifiedNameNode) getFirstChild(); + } + + @Override + protected PsiReference @NotNull [] doGetReferences() { + List result = createReferences(); + + return result.toArray(PsiReference.EMPTY_ARRAY); + } + + public PsiClass getQualifiedType() { + List result = createReferences(); + if (result.isEmpty()) { + return null; + } + + String fqn = getText().replace(" ", ""); + QualifiedNameReference ref = result.get(result.size() - 1); + PsiElement element = ref.resolve(); + + if (!(element instanceof PsiClass clazz)) { + return null; + } + + String className = clazz.getQualifiedName(); + + return className != null // + && (fqn.equals(className) // + || className.endsWith('.' + fqn)) // + ? clazz : null; + } + + protected List createReferences() { + QualifiedNameNode qnn = getQualifiedName(); + + List result = new ArrayList<>(); + createReferences(null, qnn, 0, result); + + return result; + } + + protected void createReferences( + QualifiedNameReference parentReference, QualifiedNameNode qnn, int offset, + List result + ) { + IdentifierNode identifier = qnn.getIdentifier(); + // Note: 取相对于 qnn 的 TextRange 并做偏移 + TextRange textRange = identifier.getParent().getTextRangeInParent().shiftRight(offset); + + QualifiedNameReference ref = new QualifiedNameReference(this, textRange, identifier, parentReference); + result.add(ref); + + PsiElement sub = qnn.getLastChild(); + if (!(sub instanceof QualifiedNameNode subQnn)) { + return; + } + + createReferences(ref, subQnn, subQnn.getStartOffsetInParent() + offset, result); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java new file mode 100644 index 0000000000000000000000000000000000000000..e2f98177292ce07a4e6c5997f4300cb972424de9 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java @@ -0,0 +1,25 @@ +/* + * 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.script.psi; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; + +/** + * return 语句节点 + * + * @author flytreeleft + * @date 2025-07-03 + */ +public class ReturnStatementNode extends RuleSpecNode { + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..3dcb530cc216d3f5ff1aa4124bb8411d4b773c78 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java @@ -0,0 +1,123 @@ +/* + * 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.script.psi; + +import java.util.regex.Pattern; + +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 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 implements XLangVarScope { + + public RuleSpecNode(@NotNull ASTNode node) { + super(node); + } + + /** @return 不含注释和空白节点 */ + @Override + public PsiElement @NotNull [] getChildren() { + return Trees.getChildren(this); + } + + @Override + public PsiReference @NotNull [] getReferences() { + // Note: 不能直接缓存 PsiReference,否则,容易造成数据不一致 + + // 在没有写入动作时,才执行函数并返回结果,从而避免阻塞编辑操作 + return ReadAction.compute(this::doGetReferences); + } + + protected PsiReference @NotNull [] doGetReferences() { + return PsiReference.EMPTY_ARRAY; + } + + public boolean isRuleType(RuleIElementType type) { + return getNode().getElementType() == type; + } + + /** 获取在当前节点上可见的指定变量 */ + public XLangVarDecl findVisibleVar(String varName) { + PsiElement node = this; + + while (node instanceof RuleSpecNode) { + PsiElement parent = node.getParent(); + + // Note: 下层的变量优先于上层的变量 + if (parent instanceof RuleSpecNode) { + PsiElement prev = node; + + // 从当前节点开始往前查找,并选择靠得最近的变量 + while (prev != null) { + prev = prev.getPrevSibling(); + if (!(prev instanceof XLangVarScope scope)) { + continue; + } + + XLangVarDecl var = scope.getVars().get(varName); + if (var != null) { + return var; + } + } + } + // 从所在的 标签中获取 xlib 函数的参数列表以及内置变量列表 + else if (parent instanceof XLangVarScope scope) { + XLangVarDecl var = scope.getVars().get(varName); + if (var != null) { + return var; + } + } + + node = parent; + } + return null; + } + + protected PsiClass getPsiClassByPsiType(PsiType type) { + return PsiClassHelper.getTypeClass(this, 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(this, 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 new file mode 100644 index 0000000000000000000000000000000000000000..58a6d928ce3de47c079539007cfd9f529cc290c7 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java @@ -0,0 +1,196 @@ +/* + * 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.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 { + + public StatementNode(@NotNull ASTNode node) { + super(node); + } + + /** 声明的变量,或者函数及其返回值类型,均为可访问变量 */ + @Override + 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 new file mode 100644 index 0000000000000000000000000000000000000000..4b441f72494cc836d2200d0584a6cf5bb5f5d9c7 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java @@ -0,0 +1,70 @@ +/* + * 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.script.psi; + +import java.util.Map; + +import com.intellij.lang.ASTNode; +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 等语句,各自均有唯一的根节点: + *

+ * 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 + */ +public class TopLevelStatementNode extends RuleSpecNode { + + public TopLevelStatementNode(@NotNull ASTNode node) { + super(node); + } + + @Override + public @NotNull Map getVars() { + RuleSpecNode firstChild = (RuleSpecNode) getFirstChild(); + + if (firstChild instanceof StatementNode s) { + return s.getVars(); + } // + 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/TypeNameNodePredefinedNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java new file mode 100644 index 0000000000000000000000000000000000000000..c12ea3472161cab0c2458894d0d1c4fd138f21d6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java @@ -0,0 +1,55 @@ +/* + * 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.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.idea.plugin.lang.script.reference.PredefinedTypeReference; +import org.jetbrains.annotations.NotNull; + +/** + * 预定义类型节点 + *

+ *

+ * RuleSpecNode(typeNameNode_predefined)
+ *   PsiElement('string')('string')
+ * 
+ * + * @author flytreeleft + * @date 2025-07-05 + */ +public class TypeNameNodePredefinedNode extends RuleSpecNode { + + public TypeNameNodePredefinedNode(@NotNull ASTNode node) { + super(node); + } + + public PsiElement getTypeName() { + return getLastChild(); + } + + public PsiClass getPredefinedType() { + String typeName = getTypeName().getText(); + + return PredefinedTypeReference.getPredefinedType(this, typeName); + } + + @Override + protected PsiReference @NotNull [] doGetReferences() { + PsiElement typeName = getTypeName(); + TextRange textRange = typeName.getTextRangeInParent(); + + 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/psi/VariableDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java new file mode 100644 index 0000000000000000000000000000000000000000..c02e0733ad67ed2ca8cbfa2ec092f1f9ff13a32d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java @@ -0,0 +1,69 @@ +/* + * 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.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 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 new file mode 100644 index 0000000000000000000000000000000000000000..2f20301a7490835b01d56bbf89d26087b49441c6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java @@ -0,0 +1,122 @@ +/* + * 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.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; + +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')
+ * 
+ * + * 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 + */ +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 getVars() { + IdentifierNode identifier = getIdentifier(); + ExpressionNode expression = getExpression(); + + String varName = identifier != null ? identifier.getText() : null; + PsiClass varType = expression != null ? expression.getResultType() : null; + // Note: 变量类型可以是 null,表示未知类型 + if (varName == 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 new file mode 100644 index 0000000000000000000000000000000000000000..8aa12cd08e7eb783b9cf4a3a2415676f2733cefb --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java @@ -0,0 +1,69 @@ +/* + * 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.script.psi; + +import java.util.HashMap; +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 + */ +public class VariableDeclaratorsNode extends RuleSpecNode { + + public VariableDeclaratorsNode(@NotNull ASTNode node) { + super(node); + } + + @Override + public @NotNull Map getVars() { + Map vars = new HashMap<>(); + + for (PsiElement child : getChildren()) { + if (child instanceof VariableDeclaratorNode declarator) { + vars.putAll(declarator.getVars()); + } + } + return vars; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..209c28002e0ec81c83def52cbaef73b84712732b --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java @@ -0,0 +1,48 @@ +/* + * 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.script.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +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.Nullable; + +/** + * {@link IdentifierNode} 的引用 + *

+ * 涉及对变量和类名的引用 + * + * @author flytreeleft + * @date 2025-07-06 + */ +public class IdentifierReference extends XLangReferenceBase { + private final IdentifierNode identifier; + + public IdentifierReference(PsiElement myElement, TextRange myRangeInElement, IdentifierNode identifier) { + super(myElement, myRangeInElement); + this.identifier = identifier; + } + + public IdentifierNode getIdentifier() { + return this.identifier; + } + + @Override + public @Nullable PsiElement resolveInner() { + if (!identifier.isValid()) { + return null; + } + + XLangVarDecl varDecl = identifier.getVarDecl(); + + return varDecl != null ? varDecl.element() : 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 new file mode 100644 index 0000000000000000000000000000000000000000..a0dbc723fb481be3399d5e025c9a159d3421c595 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java @@ -0,0 +1,154 @@ +/* + * 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.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; + +/** + * 对象成员(属性或方法)引用 + * + * @author flytreeleft + * @date 2025-07-06 + */ +public class ObjectMemberReference extends XLangReferenceBase { + + public ObjectMemberReference(ExpressionNode myElement) { + super(myElement, null); + } + + @Override + protected TextRange calculateDefaultRangeInElement() { + return ((ExpressionNode) myElement).getObjectMemberTextRange(); + } + + @Override + public @Nullable PsiElement resolveInner() { + return ((ExpressionNode) myElement).getObjectMember(); + } + + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + PsiElement element = myElement; + TextRange rangeInElement = getRangeInElement(); + + ElementManipulator manipulator = getManipulator(element); + element = manipulator.handleContentChange(element, rangeInElement, newElementName); + + rangeInElement = ((ExpressionNode) element).getObjectMemberTextRange(); + setRangeInElement(rangeInElement); + + 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/ObjectMethodReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java new file mode 100644 index 0000000000000000000000000000000000000000..43adf4fb69957be6e7610a4cb0dbe9bbbc1ed427 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMethodReference.java @@ -0,0 +1,31 @@ +/* + * 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.script.reference; + +import com.intellij.psi.PsiElement; +import io.nop.idea.plugin.lang.script.psi.ExpressionNode; +import org.jetbrains.annotations.Nullable; + +/** + * 对象方法引用 + * + * @author flytreeleft + * @date 2025-07-06 + */ +public class ObjectMethodReference extends ObjectMemberReference { + + public ObjectMethodReference(ExpressionNode myElement) { + super(myElement); + } + + @Override + public @Nullable PsiElement resolveInner() { + return ((ExpressionNode) myElement).getObjectMethod(); + } +} 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 0000000000000000000000000000000000000000..d58ad1088a8813d2c288e5aeb17ea856f05b1b5b --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/PredefinedTypeReference.java @@ -0,0 +1,56 @@ +/* + * 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.script.reference; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +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.Nullable; + +/** + * 对 stringnumber 等内置类型的引用 + * + * @author flytreeleft + * @date 2025-07-06 + */ +public class PredefinedTypeReference extends XLangReferenceBase { + private final PsiElement typeName; + + public PredefinedTypeReference(PsiElement myElement, TextRange myRangeInElement, PsiElement typeName) { + super(myElement, myRangeInElement); + this.typeName = typeName; + } + + @Override + public @Nullable PsiElement resolveInner() { + if (!typeName.isValid()) { + return null; + } + + return getPredefinedType(myElement, typeName.getText()); + } + + public static PsiClass getPredefinedType(PsiElement context, 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(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 new file mode 100644 index 0000000000000000000000000000000000000000..54e44be5f82db97fae8d60dcf086167b17073102 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/QualifiedNameReference.java @@ -0,0 +1,154 @@ +/* + * 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.script.reference; + +import com.intellij.codeInsight.lookup.LookupElement; +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.PsiModifier; +import com.intellij.psi.PsiNameHelper; +import com.intellij.psi.PsiPackage; +import com.intellij.psi.search.GlobalSearchScope; +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.LookupElementHelper; +import io.nop.idea.plugin.utils.PsiClassHelper; +import one.util.streamex.StreamEx; +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, TextRange myRangeInElement, // + IdentifierNode identifier, QualifiedNameReference parentReference + ) { + super(myElement, myRangeInElement); + this.identifier = identifier; + this.parentReference = parentReference; + } + + @Override + public @Nullable PsiElement resolveInner() { + if (!identifier.isValid()) { + return null; + } + + PsiElement context = myElement; + Project project = context.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(context, subName); + } + } + + return null; + } + + /** 在构造函数中,用于修改包名 */ + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + PsiElement element = myElement; + TextRange rangeInElement = getRangeInElement(); + + // 直接替换名字 + identifier.setName(newElementName); + + rangeInElement = identifier.getTextRangeInParent().shiftRight(rangeInElement.getStartOffset()); + setRangeInElement(rangeInElement); + + return element; + } + + /** 在构造函数中,用于修改类名 */ + @Override + 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; + } + + @Override + public Object @NotNull [] getVariants() { + // Note: 只有在当前引用的结果不存在时,才需要补全,而其可补全项由上层引用结果确定 + PsiElement context = parentReference != null ? parentReference.resolve() : null; + if (context == null) { + return LookupElement.EMPTY_ARRAY; + } + + GlobalSearchScope scope = PsiClassHelper.getSearchScope(context); + + StreamEx result = null; + if (context instanceof PsiPackage pkg) { + String pkgName = pkg.getQualifiedName(); + + StreamEx pkgStream = StreamEx.of(pkg.getSubPackages(scope)).filter(p -> { + String shortName = p.getQualifiedName().substring(pkgName.length() + 1); + + return PsiNameHelper.getInstance(p.getProject()).isIdentifier(shortName); + }); + + StreamEx classStream = StreamEx.of(pkg.getClasses(scope)); + + StreamEx result0 = LookupElementHelper.lookupPsiPackagesStream(pkgStream); + StreamEx result1 = LookupElementHelper.lookupPsiClassesStream(classStream); + + result = StreamEx.of(result0).append(result1); + + } // + else if (context instanceof PsiClass clazz) { + StreamEx stream = StreamEx.of(clazz.getInnerClasses()) + .filter(c -> c.hasModifierProperty(PsiModifier.STATIC)); + + result = LookupElementHelper.lookupPsiClassesStream(stream); + } + + return result != null ? result.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 0000000000000000000000000000000000000000..952bc0b9815d2041852d79ba41b2e6f34bcfb405 --- /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 0000000000000000000000000000000000000000..c340da2e477278cb0b1dd3b6a02c19dbeca89587 --- /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/link/XLangFileDeclarationHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/link/XLangFileDeclarationHandler.java deleted file mode 100644 index 61cf27f7ddb647af7bffc3bec2d9a5aa490fa7ae..0000000000000000000000000000000000000000 --- 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/refactoring/AntRenameHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/refactoring/AntRenameHandler.java deleted file mode 100644 index 31d9f61f341a49a3ba59134d4756610bbcda3059..0000000000000000000000000000000000000000 --- 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/resource/EnumDictBean.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictBean.java new file mode 100644 index 0000000000000000000000000000000000000000..a266daff59bf0b16643eda7f9938da529739fbdd --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictBean.java @@ -0,0 +1,17 @@ +/* + * 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.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/EnumDictOptionBean.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictOptionBean.java new file mode 100644 index 0000000000000000000000000000000000000000..c78c4f32ef5b162cdc8053a6bde82449dbbb514d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/resource/EnumDictOptionBean.java @@ -0,0 +1,28 @@ +/* + * 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.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 05c83eb4db9b6ffac9e107e292352708df2914a2..b39282fce436e7362005a140f732f89f19f708e7 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,12 +7,11 @@ */ 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; @@ -21,8 +20,8 @@ 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 +31,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 +61,65 @@ 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); + + DictModel dictModel = new DictModelParser().parseFromResource(resource); + dictModel.getDictBean().setLocation(dictModel.getLocation()); + + return dictModel; } - } 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)) { + GlobalSearchScope scope = GlobalSearchScope.allScope(project); + + PsiClass clazz = PsiClassHelper.findClass(project, dictName, scope); + 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 EnumDictBean(); + 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/services/NopAppListener.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopAppListener.java index e37efe5842447e2717cd5ecfaf11779ce6b629b9..ee90c8243339f3136cc1088623898da5712e733c 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,10 +7,13 @@ */ 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; 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; @@ -20,11 +23,24 @@ 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) -> { + 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); 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 87e3d940bea85a4c63ff2b675db6482cc75f2f7f..e5fb2f64ecdfe080dbb7f6697d6e1993b8c48b4e 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 new file mode 100644 index 0000000000000000000000000000000000000000..3db8a015f33bd72501533644e771436bbae0a51a --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java @@ -0,0 +1,89 @@ +/* + * 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.utils; + +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; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiPackage; +import com.intellij.util.PlatformIcons; +import io.nop.api.core.beans.DictOptionBean; +import io.nop.api.core.convert.ConvertHelper; +import one.util.streamex.StreamEx; + +/** + * @author flytreeleft + * @date 2025-07-21 + */ +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(); + + return LookupElementBuilder.create(optValue) + // icon 靠左布局 + .withIcon(PlatformIcons.ENUM_ICON) + // type text 靠后布局 + .withTypeText(label) +// // tail text 与 lookup string 紧挨着 +// .withTailText(label) // +// // presentable text 将替换 lookup string 作为最终的显示文本 +// .withPresentableText(label) // + ; + } + + public static StreamEx lookupPsiPackagesStream(StreamEx stream) { + return lookupPsiPackagesStream(stream, + (p) -> LookupElementBuilder.create(p) + .withIcon(p.getIcon(Iconable.ICON_FLAG_VISIBILITY))); + } + + public static StreamEx lookupPsiPackagesStream( + StreamEx stream, Function builder + ) { + return stream.distinct(PsiPackage::getQualifiedName) + .sorted(Comparator.comparing(PsiPackage::getQualifiedName)) + .map(builder); + } + + public static StreamEx lookupPsiClassesStream(StreamEx stream) { + return lookupPsiClassesStream(stream, + (c) -> JavaClassNameCompletionContributor.createClassLookupItem(c, false)); + } + + public static StreamEx lookupPsiClassesStream( + StreamEx stream, Function builder + ) { + return stream.filter(c -> c.getQualifiedName() != null && StringUtil.isNotEmpty(c.getName())) + .distinct(PsiClass::getQualifiedName) + .sorted(Comparator.comparing(PsiClass::getQualifiedName)) + .map(builder); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/MarkdownHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/MarkdownHelper.java index 2724c31d9d7e8a570e969e92f2622a7611e3c798..bc2ed0b996b28d6c7f8b45de5c240d59afc3d045 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/MarkdownHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/MarkdownHelper.java @@ -1,3 +1,11 @@ +/* + * 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.utils; import java.util.Arrays; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/ProjectFileHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/ProjectFileHelper.java index ceb92e73dd3758e23a21231f0c4b99937ce268bf..1f4043c8a5e73ab420c4e1ccd88e67d520b34920 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/ProjectFileHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/ProjectFileHelper.java @@ -2,6 +2,14 @@ package io.nop.idea.plugin.utils; +import java.io.File; +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + import com.intellij.openapi.application.PathManager; import com.intellij.openapi.application.PluginPathManager; import com.intellij.openapi.editor.Document; @@ -13,20 +21,24 @@ import com.intellij.psi.PsiDocumentManager; 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.util.PsiTreeUtil; import com.intellij.psi.xml.XmlElement; +import com.intellij.util.containers.CollectionFactory; import com.intellij.xdebugger.XDebuggerUtil; import com.intellij.xdebugger.XSourcePosition; +import io.nop.api.core.beans.DictBean; import io.nop.api.core.exceptions.NopException; import io.nop.api.debugger.LineLocation; import io.nop.commons.util.StringHelper; +import io.nop.core.dict.DictProvider; import io.nop.core.resource.ResourceHelper; +import io.nop.idea.plugin.resource.ProjectEnv; import org.jetbrains.annotations.Nullable; -import java.io.File; -import java.net.URL; - public class ProjectFileHelper { + /** * 与FileHelper.getFileUrl格式保持一致 */ @@ -50,16 +62,18 @@ public class ProjectFileHelper { } public static LineLocation toLineLocation(XSourcePosition pos) { - if (pos == null) + if (pos == null) { return null; + } final VirtualFile file = pos.getFile(); final String fileURL = ProjectFileHelper.getFileUrl(file); return new LineLocation(fileURL, pos.getLine() + 1); } public static String getNopVfsPath(VirtualFile file) { - if (file == null) + if (file == null) { return null; + } String protocol = file.getFileSystem().getProtocol(); String path = file.getPath(); @@ -70,21 +84,75 @@ public class ProjectFileHelper { } } int pos = path.indexOf("/_vfs/"); - if (pos < 0) + if (pos < 0) { return null; + } return path.substring(pos + "/_vfs/".length() - 1); } public static String getNopVfsStdPath(VirtualFile file) { String path = getNopVfsPath(file); - if (path == null) + if (path == null) { return null; + } return ResourceHelper.getStdPath(path); } + /** 查找所有的 *.xdef 资源路径 */ + public static Collection findAllXdefNopVfsPaths(Project project) { + return FilenameIndex.getAllFilesByExt(project, "xdef") + .stream() + .map(ProjectFileHelper::getNopVfsStdPath) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + /** 查找所有的 Nop 字典资源路径 */ + public static Collection findAllDictNopVfsPaths(Project project) { + return FilenameIndex.getAllFilesByExt(project, "dict.yaml") + .stream() + .map(ProjectFileHelper::getNopVfsStdPath) + .filter(Objects::nonNull) + .filter((path) -> path.startsWith("/dict/")) + .collect(Collectors.toSet()); + } + + /** 从 vfs 路径中获取字典名字 */ + public static String getDictNameFromVfsPath(String path) { + String name = StringHelper.removeHead(path, "/dict/"); + name = StringHelper.removeTail(name, ".dict.yaml"); + + return name; + } + + /** 查找所有 vfs 资源路径 */ + public static Collection findAllNopVfsPaths(Project project) { + Set names = CollectionFactory.createSmallMemoryFootprintSet(); + Collections.addAll(names, FilenameIndex.getAllFilenames(project)); + + Set vfsPaths = CollectionFactory.createSmallMemoryFootprintSet(); + + GlobalSearchScope scope = GlobalSearchScope.allScope(project); + FilenameIndex.processFilesByNames(names, true, scope, null, (file) -> { + String vfsPath = getNopVfsPath(file); + if (vfsPath != null) { + vfsPaths.add(vfsPath); + } + return true; + }); + + return vfsPaths; + } + + public static DictBean loadDict(PsiElement refElement, String dictName) { + return ProjectEnv.withProject(refElement.getProject(), + () -> DictProvider.instance().getDict(null, dictName, null, null)); + } + public static XSourcePosition buildPos(LineLocation loc) { - if (loc == null) + if (loc == null) { return null; + } return buildPos(loc.getSourcePath(), loc.getLine()); } @@ -93,8 +161,9 @@ public class ProjectFileHelper { path = StringHelper.decodeURL(path); } VirtualFile file = ProjectFileHelper.getVirtualFile(path); - if (file == null) + if (file == null) { return null; + } return XDebuggerUtil.getInstance().createPosition(file, line - 1); } @@ -118,8 +187,9 @@ public class ProjectFileHelper { public static CharSequence getLine(Document document, int line) { int beginOffset = document.getLineStartOffset(line); int endOffset = document.getLineEndOffset(line); - if (beginOffset < 0 || endOffset < 0 || beginOffset >= endOffset) + if (beginOffset < 0 || endOffset < 0 || beginOffset >= endOffset) { return null; + } CharSequence str = document.getCharsSequence().subSequence(beginOffset, endOffset); return str; @@ -156,7 +226,8 @@ public class ProjectFileHelper { /** * 根据外部文件路径查找到对应VirtualFile,并更新IDE内的缓存 * - * @param file 外部文件路径 + * @param file + * 外部文件路径 * @return file对应的VirtualFile */ public static VirtualFile refreshFile(File file) { @@ -168,8 +239,9 @@ public class ProjectFileHelper { } public static VirtualFile getVirtualFile(String path) { - if (StringHelper.isEmpty(path)) + if (StringHelper.isEmpty(path)) { return null; + } if (path.startsWith("jar:")) { if (path.startsWith("jar:file:")) { @@ -193,7 +265,8 @@ public class ProjectFileHelper { /** * 得到IDE指定插件的根目录 * - * @param pluginName 插件名称 + * @param pluginName + * 插件名称 * @return 当前IDE的plugins目录下的指定子目录 */ public static File getPluginHome(String pluginName) { 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 0000000000000000000000000000000000000000..fd42fa02dbb79e38f5ecd03443d25637348614bf --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/PsiClassHelper.java @@ -0,0 +1,372 @@ +/* + * 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.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.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; +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.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.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; +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 { + 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", + 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"); + + static { + // 支持解析包名:JavaClassReference#advancedResolveInner + javaClassRefProvider.setOption(JavaClassReferenceProvider.ADVANCED_RESOLVE, true); + } + + public static @NotNull GlobalSearchScope getSearchScope(@NotNull PsiElement element) { + Project project = element.getProject(); + + return javaClassRefProvider.getScope(project); + } + + public static PsiReference @NotNull [] createJavaClassReferences( + PsiElement element, String qualifiedName, int startInElement + ) { + JavaClassReferenceSet refSet = new JavaClassReferenceSet(qualifiedName, + element, + startInElement, + false, + javaClassRefProvider); + + 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) { + return null; + } + + // 处理通配符泛型 + if (type instanceof PsiWildcardType t) { + PsiType bound = t.getBound(); + + return bound != null + ? getTypeClass(context, 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(context, 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 findClass(context, wrapperName); + } + return null; + } + // 处理数组类型 + else if (type instanceof PsiArrayType t) { + return getTypeClass(context, t.getComponentType()); + } + // 处理类类型(包括泛型) + else if (type instanceof PsiClassType t) { + PsiClass clazz = t.resolve(); + // 泛型参数 + PsiType[] parameters = t.getParameters(); + + if (clazz != null && parameters.length > 0) { + // List -> 返回 String.class + if (CommonClassNames.JAVA_UTIL_LIST.equals(clazz.getQualifiedName())) { + return getTypeClass(context, parameters[0]); + } + + // 自定义泛型类 + PsiTypeParameter[] typeParams = clazz.getTypeParameters(); + if (typeParams.length > 0) { + // 查找实际使用的类型参数 + for (int i = 0; i < typeParams.length; i++) { + if (i < parameters.length) { + PsiClass resolved = getTypeClass(context, parameters[i]); + + if (resolved != null) { + return resolved; + } + } + } + } + } + return clazz; + } + + return null; + } + + /** + * 查找项目中 {@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<>(); + + Query query = findInheritors(project, IStdDomainHandler.class.getName()); + query.filtering((clazz) -> !clazz.isInterface() + && !clazz.isEnum() + && !clazz.isAnnotationType() + && !clazz.hasModifierProperty(PsiModifier.ABSTRACT) // + ) // + .forEach((clazz) -> { + Object name = getMethodReturnConstantValue(clazz, "getName"); + + if (name != null) { + map.computeIfAbsent(name.toString(), (k) -> new ArrayList<>()).add(clazz); + } + }); + + return map; + } + + public static PsiClass findClass(PsiElement context, String className) { + Project project = context.getProject(); + GlobalSearchScope scope = PsiClassHelper.getSearchScope(context); + + return findClass(project, className, scope); + } + + public static PsiClass findClass(Project project, String className) { + GlobalSearchScope scope = GlobalSearchScope.allScope(project); + + 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) { + if (StringHelper.isEmpty(pkgName)) { + return null; + } + return JavaPsiFacade.getInstance(project).findPackage(pkgName); + } + + /** 查找指定类的继承类 */ + public static @NotNull Query findInheritors(Project project, String className) { + GlobalSearchScope scope = GlobalSearchScope.allScope(project); + + return findInheritors(project, className, scope); + } + + /** 查找指定类的继承类 */ + public static @NotNull Query findInheritors(Project project, String className, GlobalSearchScope scope) { + // 确保可找到基类 + GlobalSearchScope baseScope = GlobalSearchScope.allScope(project); + + PsiClass clazz = findClass(project, className, baseScope); + if (clazz == null) { + return EmptyQuery.getEmptyQuery(); + } + + return ClassInheritorsSearch.search(clazz, scope, true); + } + + /** 获取指定方法返回的常量值 */ + public static Object getMethodReturnConstantValue(PsiClass clazz, String methodName) { + PsiMethod[] methods = clazz.findMethodsByName(methodName, true); + if (methods.length == 0) { + return null; + } + + PsiMethod method = methods[0]; + ReturnStatementAnalyzer analyzer = new ReturnStatementAnalyzer(); + method.getBody().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; + } + + // 处理字面量表达式 + 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); + } + + 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 computeConstantExpression(returnExpr); + } + } +} 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 2d582726f01087097a65fd1f4f9f290f8c1ebd5a..0000000000000000000000000000000000000000 --- 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/utils/XDefPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XDefPsiHelper.java index e8e514200e2a4c922e57042d5cc292bc77bbb58d..2ccea4ffe5c09765d25f84c1b15a344589e9acbc 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,12 +7,12 @@ */ package io.nop.idea.plugin.utils; -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.parse.XDefinitionParser; @@ -22,11 +22,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 { static final Logger LOG = LoggerFactory.getLogger(XDefPsiHelper.class); @@ -34,127 +29,89 @@ 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) { - IXDefinition xdef = new XDefinitionParser().parseFromResource(new ClassPathResource("classpath:/_vfs/nop/schema/xdef.xdef")); - xdefDef = xdef; + xdefDef = loadDef(XDslConstants.XDSL_SCHEMA_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 = loadDef(XDslConstants.XDSL_SCHEMA_XDSL); } 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 = loadDef(XDslConstants.XDSL_SCHEMA_XPL); } return xplDef; } + public static String getXDslNamespace(XmlTag tag) { + XmlTag rootTag = XmlPsiHelper.getRoot(tag); + String ns = XmlPsiHelper.getXmlnsForUrl(rootTag, XDslConstants.XDSL_SCHEMA_XDSL); - public static String getSchemaPath(XmlTag tag) { - PsiFile file = tag.getContainingFile(); + if (ns == null) { + ns = XDslKeys.DEFAULT.NS; + } + return ns; + } + + /** 从根节点获取 dsl 的元模型的 vfs 路径 */ + 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_XDEF); - String key; - if (ns == null) { - key = XDslKeys.DEFAULT.SCHEMA; - } else { - key = ns + ":schema"; - } - String schemaPath = tag.getAttributeValue(key); - return schemaPath; + String ns = getXDslNamespace(rootTag); + String key = ns + ":schema"; + + String schemaUrl = rootTag.getAttributeValue(key); + // Note: schema 可能为相对路径 + return XmlPsiHelper.getNopVfsAbsolutePath(schemaUrl, rootTag); } 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; } } - 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); - } - } - return null; - } + public static IXDefinition loadSchema(PsiFile file) { + String content = file.getText(); + // Note: 解析过程中,会检查路径的有效性,需保证以 / 开头,并添加 .xdef 后缀 + IResource resource = new InMemoryTextResource("/" + file.getText().hashCode() + ".xdef", content); - public static XmlTagInfo getTagInfo(String schemaUrl, XmlTag tag) { - IXDefinition def = loadSchema(schemaUrl); - if (def == null) + try { + return new XDefinitionParser().parseFromResource(resource); + } catch (Exception e) { + LOG.debug("nop.load-schema-fail", e); return null; - - IXDefNode dslDefNode = getXDslDef().getRootNode(); - IXDefNode xplDefNode = getXplDef().getRootNode().getChild("div"); - - List tags = getSelfAndParents(tag); - XmlTagInfo tagInfo = null; - tags = CollectionHelper.reverseList(tags); - - boolean xpl = false; - 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); - } else { - XmlTagInfo parent = tagInfo; - String tagName = xmlTag.getName(); - if (tagName.startsWith("xdsl:")) - tagName = "x:" + tagName.substring("xdsl:".length()); - - dslDefNode = parent.getDslNodeChild(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); - - if (isXplNode(defNode)) { - xpl = true; - } - } } - return tagInfo; } - static boolean isXplNode(IXDefNode defNode) { - if (defNode == null) - return false; - if (defNode.getXdefValue() == null) - return false; - String stdDomain = defNode.getXdefValue().getStdDomain(); - return stdDomain.equals("xpl") || stdDomain.startsWith("xpl-"); + public static boolean isXplDefNode(IXDefNode defNode) { + String stdDomain = getDefNodeType(defNode); + + return stdDomain != null && (stdDomain.equals("xpl") || stdDomain.startsWith("xpl-")); } - static List getSelfAndParents(XmlTag tag) { - List ret = new ArrayList<>(); - while (tag != null) { - ret.add(tag); - tag = tag.getParentTag(); + public static String getDefNodeType(IXDefNode defNode) { + if (defNode == null || defNode.getXdefValue() == null) { + return null; } - return ret; + return defNode.getXdefValue().getStdDomain(); } -} \ No newline at end of file +} 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 5deb2072528b25be0fc07d1a6ffe2c2aca505943..1247ff901ef2c6ec4b12305d37ff80c6ea28c2cf 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,155 +7,129 @@ */ package io.nop.idea.plugin.utils; -import com.intellij.lang.ASTNode; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + 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; 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; +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 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 org.jetbrains.annotations.NotNull; 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) { + 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 前缀 + return path != null ? path.replace("classpath:_vfs", "") : null; + } + + public static String getNopVfsPath(PsiElement element) { + PsiFile file = element.getContainingFile(); + if (file == null) { + return null; + } + + VirtualFile vf = file.getVirtualFile(); + // Note: 在编辑过程中得到的 VirtualFile 可能为 null,需尝试通过 + // PsiFile#getOriginalFile 获得 VirtualFile + if (vf == null && file.getOriginalFile() != file) { + vf = file.getOriginalFile().getVirtualFile(); + } + + return vf != null ? ProjectFileHelper.getNopVfsPath(vf) : null; + } + + /** + * 获取 path 的 vfs 绝对路径。 + * 若 path 为相对路径,则视为其相对于 element 所在文件的目录 + */ + 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); - 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; } - public static PsiFile[] findPsiFile(Project project, String path) { - List list = findPsiFileList(project, path); - if (list.isEmpty()) - return EMPTY_FILES; - return list.toArray(EMPTY_FILES); - } - - 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(); + public static List findPsiFilesByNopVfsPath(PsiElement element, String path) { + if (element == null || path == null) { + return List.of(); } - 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); - } - } + Project project = element.getProject(); + String absPath = getNopVfsAbsolutePath(path, element); - PsiFile[] files = FilenameIndex.getFilesByName(project, ns + ".xlib", GlobalSearchScope.allScope(project)); - return files.length == 0 ? Collections.emptyList() : Arrays.asList(files); + return findPsiFileList(project, absPath); } - public static PsiElement[] findXplTag(Project project, XmlTag tag) { - List files = findXplLib(project, tag); - if (files.isEmpty()) - return EMPTY_ELEMENTS; - - 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); + /** 获取指定行列的 {@link PsiElement 元素} */ + public static PsiElement getPsiElementAt(PsiFile psiFile, int line, int column) { + Document document = PsiDocumentManager.getInstance(psiFile.getProject()).getDocument(psiFile); + if (document == null) { + return null; } - return ret.toArray(EMPTY_ELEMENTS); - } - 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; + int offset = document.getLineStartOffset(line - 1) + column - 1; + return psiFile.findElementAt(offset); } - public static String getNopVfsPath(PsiElement element) { - PsiFile file = element.getContainingFile(); - if (file == null) - return null; + /** 获取指定位置的 {@link PsiElement 元素} */ + public static PsiElement getPsiElementAt(PsiFile psiFile, SourceLocation loc) { + return getPsiElementAt(psiFile, loc.getLine(), loc.getCol()); + } - VirtualFile vf = file.getVirtualFile(); - if (vf == null) - return null; + /** 获取指定行列的指定类型的 {@link PsiElement 元素} */ + public static T getPsiElementAt(PsiFile psiFile, SourceLocation loc, Class type) { + PsiElement element = getPsiElementAt(psiFile, loc); - return ProjectFileHelper.getNopVfsPath(vf); + if (type.isInstance(element)) { + return (T) element; + } + return PsiTreeUtil.getParentOfType(element, type); } public static SourceLocation getLocation(PsiElement element) { @@ -163,19 +137,18 @@ public class XmlPsiHelper { } 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()); } 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); @@ -186,90 +159,113 @@ 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 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; } - public static String getAttrName(XmlAttributeValue value) { - if (value == null) - return null; - - if (!(value.getParent() instanceof XmlAttribute)) - return null; + public static Set getChildTagNames(XmlTag tag) { + Set names = new HashSet<>(); - XmlAttribute attr = (XmlAttribute) value.getParent(); - return attr.getName(); + for (PsiElement element : tag.getChildren()) { + if (element instanceof XmlTag) { + names.add(((XmlTag) element).getName()); + } + } + return names; } - public static XmlAttribute getAttr(XmlAttributeValue value) { - if (value == null) - return null; - - if (!(value.getParent() instanceof XmlAttribute)) - return null; + public static Set getTagAttrNames(XmlTag tag) { + Set names = new HashSet<>(); - XmlAttribute attr = (XmlAttribute) value.getParent(); - return attr; + for (XmlAttribute attr : tag.getAttributes()) { + names.add(attr.getName()); + } + return names; } - public static boolean hasChild(XmlTag tag) { - return tag.getNode().findChildByType(XmlElementType.XML_TAG) != null; - } + /** + * 根据属性值获取匹配的子节点,在 attrNamenull 时,匹配节点的标签名 + *

+ * 其逻辑等价于 {@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; + } - 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; + if (attrName == null) { + if (child.getName().equals(attrValue)) { + return child; + } + } else if (attrValue.equals(child.getAttributeValue(attrName))) { + return child; + } + } + return null; } - public static boolean isElementType(PsiElement elm, IElementType type) { - if (elm == null) - return false; - ASTNode node = elm.getNode(); - if (node == null) - return false; + /** 根据查找子节点上指定名字的属性 */ + public static List getAttrsFromChildTag(XmlTag tag, String attrName) { + List attrs = new ArrayList<>(); + + for (PsiElement element : tag.getChildren()) { + if (!(element instanceof XmlTag child)) { + continue; + } - return node.getElementType() == type; + XmlAttribute attr = child.getAttribute(attrName); + if (attr != null) { + attrs.add(attr); + } + } + return attrs; } - public static Set getChildTagNames(XmlTag tag) { - Set tagNames = new HashSet<>(); + /** 从子节点上查找公共的属性名 */ + public static List getCommonAttrNamesFromChildTag(XmlTag tag) { + List attrNames = new ArrayList<>(); + for (PsiElement element : tag.getChildren()) { - if (element instanceof XmlTag) { - tagNames.add(((XmlTag) element).getName()); + if (!(element instanceof XmlTag child)) { + continue; + } + + Set names = getTagAttrNames(child); + if (attrNames.isEmpty()) { + attrNames.addAll(names); + } else { + attrNames.retainAll(names); } } - return tagNames; + return attrNames; } - public static XmlTag getXmlTag(PsiElement element) { - if (element == null) - return null; - - if (element instanceof XmlTag) - return ((XmlTag) element); + /** 找到第一个符合条件的 {@link PsiElement 元素} */ + public static T findFirstElement( + PsiElement element, Predicate condition + ) { + PsiElement[] result = new PsiElement[] { null }; - do { - PsiElement parent = element.getParent(); - if (parent == null) - return null; - if (parent instanceof XmlTag) - return (XmlTag) parent; - element = parent; - } while (true); + PsiTreeUtil.processElements(element, el -> { + if (condition.test(el)) { + result[0] = el; + return false; + } + return true; // 继续遍历 + }); + return (T) result[0]; } public static XmlTag getRoot(XmlTag tag) { do { XmlTag parent = tag.getParentTag(); - if (parent == null) + if (parent == null) { return tag; + } tag = parent; } while (true); } @@ -278,8 +274,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/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 29a0ad26fe9d70112a599a5f0649cc4ca6f93cdf..0000000000000000000000000000000000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlTagInfo.java +++ /dev/null @@ -1,88 +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.IXDefNode; -import io.nop.xlang.xdef.IXDefinition; -import io.nop.xlang.xdef.domain.StdDomainRegistry; - -public class XmlTagInfo { - private final XmlTag tag; - private final IXDefNode defNode; - private final IXDefNode parentDefNode; - private final boolean custom; - private final IXDefNode dslNode; - private final 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; - this.custom = custom; - this.dslNode = dslNode; - this.xdef = xdef; - } - - public boolean isAllowedUnknownName(String name) { - if (!StringHelper.hasNamespace(name)) - return false; - - String ns = StringHelper.getNamespace(name); - if (xdef.getXdefCheckNs() == null || xdef.getXdefCheckNs().isEmpty()) - return true; - - return !xdef.getXdefCheckNs().contains(ns); - } - - public IXDefinition getXdef() { - return xdef; - } - - public IXDefNode getDslNode() { - return dslNode; - } - - public IXDefNode getParentDefNode() { - return parentDefNode; - } - - public IXDefNode getDslNodeChild(String tagName) { - if (dslNode == null) - return null; - return dslNode.getChild(tagName); - } - - public IXDefNode getDefNodeChild(String tagName) { - if (defNode == null) - return null; - return defNode.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 XmlTag getTag() { - return tag; - } - - public IXDefNode getDefNode() { - return defNode; - } -} 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 0000000000000000000000000000000000000000..12b8cafab902445a37cf1903e5535b11871145a1 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFile.java @@ -0,0 +1,229 @@ +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.*; + +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 PsiElement srcElement; + private final String path; + private final Function targetResolver; + + private PsiElement[] children; + + /** @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("/"); + + this.targetResolver = targetResolver; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + getPath(); + } + + @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; + } + + /** {@link #getChildren()} 是否为 {@link PsiFile} */ + public boolean forFileChildren() { + return targetResolver == null; + } + + @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); + } + return children; + } + + @Override + public @NotNull Project getProject() { + return srcElement.getProject(); + } + + public String getPath() { + return path; + } + + @Override + public String getName() { + return getPath(); + } + + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + @Override + public PsiElement setName(@NotNull String name) throws IncorrectOperationException { + // 对当前元素本身不做更名 + 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() { + 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 0000000000000000000000000000000000000000..f485bcbde5c9d9d185f39b7034a65b02c9296e91 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/vfs/NopVirtualFileReference.java @@ -0,0 +1,53 @@ +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.ProjectFileHelper; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import org.jetbrains.annotations.NotNull; +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() { + String absPath = XmlPsiHelper.getNopVfsAbsolutePath(path, myElement); + + NopVirtualFile target = new NopVirtualFile(myElement, absPath); + + if (target.hasEmptyChildren()) { + String msg = NopPluginBundle.message("xlang.annotation.reference.vfs-file-not-found", path); + setUnresolvedMessage(msg); + + return null; + } + return target; + } + + @Override + public Object @NotNull [] getVariants() { + Project project = myElement.getProject(); + + return ProjectFileHelper.findAllNopVfsPaths(project).stream().sorted().toArray(); + } +} 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 d348a236f02cc4811e550e30e9ce9049c9fc0669..0000000000000000000000000000000000000000 --- 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(); - } -} 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 3177388588efcd44b34d3e57af7fe84b66fc42f6..60c792532dadf03ba4c717eb9f97aa0565aa97e4 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 @@ -73,39 +74,54 @@ + + + + + + + + - - + + - - - - - - - - - - - - - - + - - - - - - + + + + + + + + + + + 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 8fb78732543d29043bf55d56f8b9dea414bd3356..5d0fde4402d1816988a346ba44d8ce2da2fd182c 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,37 @@ -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.doc.markdown.link-title='{'Link'}'{0} -xlang.doc.markdown.image-title='{'Image'}'{0} +line.breakpoints.tab.title = XLang line breakpoints +xlang.annotation.attr.not-defined = Undefined attribute ''{0}'' +xlang.annotation.attr.value-required = Attribute ''{0}'' can''t have empty value +xlang.annotation.tag.not-defined = Undefined tag ''{0}'' +xlang.annotation.tag.value-not-allowed = Tag ''{0}'' can''t have any value +xlang.annotation.tag.child-not-allowed = Tag ''{0}'' can''t have any child +xlang.annotation.tag.body-required = Tag ''{0}'' body can''t be empty +xlang.annotation.reference.vfs-file-not-found = Vfs file ''{0}'' doesn''t exist +xlang.annotation.reference.dict-not-found = Dict or enum ''{0}'' doesn''t exist +xlang.annotation.reference.dict-yaml-not-found = Dict yaml file ''{0}'' doesn''t exist +xlang.annotation.reference.dict-option-not-defined = Dict option ''{0}'' isn''t defined in ''{1}'' +xlang.annotation.reference.enum-option-not-defined = Enum item ''{0}'' isn''t listed in {1} +xlang.annotation.reference.xdef-ref-not-found = No xdef defined node ''{0}'' exists +xlang.annotation.reference.xdef-ref-not-found-in-path = No xdef defined node ''{0}'' in ''{1}'' +xlang.annotation.reference.parent-tag-attr-not-found = No attribute ''{0}'' in current node +xlang.annotation.reference.parent-tag-attr-self-referenced = Attribute ''{0}'' is self-referenced +xlang.annotation.reference.xdef-key-attr-not-found = No child node which has attribute ''{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 <{0}/> exists +xlang.annotation.reference.x-prototype-attr-not-found = No sibling node [{0}="{1}"] exists +xlang.annotation.reference.x-prototype-tag-self-referenced = Node <{0}/> is self-referenced +xlang.annotation.reference.x-prototype-attr-self-referenced = Node [{0}="{1}"] is self-referenced +xlang.annotation.reference.attr-xdef-not-defined = Undefined attribute ''{0}'' +xlang.annotation.reference.std-domain-not-registered = Std-domain ''{0}'' isn''t listed in {1} +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) +xlang.doc.flag.required = [Required] +xlang.doc.flag.option = [Option] +xlang.doc.flag.internal = [Internal] +xlang.doc.flag.allow-cp-expr = [#{var}] +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 e06849a87b726074fa4983f38b4cd62c534ca7ea..cf1366f2c7d0a503b4365492611aefe3d646af3a 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,37 @@ -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.doc.markdown.link-title='{'\u94FE\u63A5'}'{0} -xlang.doc.markdown.image-title='{'\u56FE\u7247'}'{0} +line.breakpoints.tab.title = XLang line breakpoints +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.tag.not-defined = \u6807\u7B7E ''{0}'' \u672A\u5B9A\u4E49 +xlang.annotation.tag.value-not-allowed = \u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5305\u542B\u5185\u5BB9 +xlang.annotation.tag.child-not-allowed = \u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u5305\u542B\u5B50\u8282\u70B9 +xlang.annotation.tag.body-required = \u6807\u7B7E ''{0}'' \u4E0D\u5141\u8BB8\u4E3A\u7A7A +xlang.annotation.reference.vfs-file-not-found = vfs \u6587\u4EF6 ''{0}'' \u4E0D\u5B58\u5728 +xlang.annotation.reference.dict-not-found = \u5B57\u5178\u6216\u679A\u4E3E\u7C7B ''{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.parent-tag-attr-self-referenced = \u5F53\u524D\u5C5E\u6027 ''{0}'' \u51FA\u73B0\u81EA\u5F15\u7528 +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\u5144\u5F1F\u8282\u70B9 <{0}/> +xlang.annotation.reference.x-prototype-attr-not-found = \u4E0D\u5B58\u5728\u5144\u5F1F\u8282\u70B9 [{0}="{1}"] +xlang.annotation.reference.x-prototype-tag-self-referenced = \u5F53\u524D\u8282\u70B9 <{0}/> \u51FA\u73B0\u81EA\u5F15\u7528 +xlang.annotation.reference.x-prototype-attr-self-referenced = \u5F53\u524D\u8282\u70B9 [{0}="{1}"] \u51FA\u73B0\u81EA\u5F15\u7528 +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.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 +xlang.doc.flag.required = [\u5FC5\u586B] +xlang.doc.flag.option = [\u53EF\u9009] +xlang.doc.flag.internal = [\u5185\u90E8] +xlang.doc.flag.allow-cp-expr = [#{var}] +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 new file mode 100644 index 0000000000000000000000000000000000000000..a77eddbcfcbb7bb58b18c97ba8878fc8a34f8c36 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -0,0 +1,262 @@ +/* + * 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; + +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.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.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.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; +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.impl.ClassPathResource; +import io.nop.idea.plugin.lang.XLangFileType; +import io.nop.idea.plugin.lang.reference.XLangReference; +import io.nop.idea.plugin.services.NopAppListener; + +/** + * @author flytreeleft + * @date 2025-06-17 + */ +public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtureTestCase { + private static final String XLANG_EXT = "xtest"; + + 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(() -> { + // 将真实的 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"); + + FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, XLANG_EXT); + + new NopAppListener().appFrameCreated(new ArrayList<>()); + + // Note: 提前将被引用的 vfs 资源添加到 Project 中 + addAllNopXDefsToProject(); + addVfsResourcesToProject("/nop/core/xlib/meta-gen.xlib", "/dict/core/std-domain.dict.yaml"); + addAllTestVfsResourcesToProject(); + }); + } + + @Override + protected void tearDown() throws Exception { + ApplicationManager.getApplication().runWriteAction(() -> { + cleanup.cancel(); + }); + + super.tearDown(); + } + + protected PsiFile configureByXLangText(String text) { + return 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 以确保目标文件已存在 + */ + protected void addVfsResourcesToProject(String... resources) { + for (String resource : resources) { + String text = readVfsResource(resource); + + 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 = new ClassPathResource("classpath:_vfs" + resource); + return ResourceHelper.readText(res); + } + + protected String insertCaretIntoVfs(String resource, String insertAt, String replacement) { + return readVfsResource(resource).replace(insertAt, replacement); + } + + protected PsiElement getElementAtCaret() { + doAssertCaretExists(); + + return myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + } + + /** 找到光标位置的 {@link XLangReference} 或者其他类型的唯一引用 */ + protected PsiReference findReferenceAtCaret() { + doAssertCaretExists(); + + // 实际有多个引用时,将构造返回 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 getDocAtCaret() { + // Note: 通过 ApplicationManager.getApplication().runReadAction(() -> {}) + // 消除异常 "Read access is allowed from inside read-action" + PsiElement originalElement = getElementAtCaret(); + + PsiElement element = DocumentationManager.getInstance(getProject()) + .findTargetElement(myFixture.getEditor(), myFixture.getFile()); + + DocumentationProvider docProvider = DocumentationManager.getProviderFromElement(originalElement); + + return docProvider.generateDoc(element, originalElement); + } + + private void doAssertCaretExists() { + assertTrue("No '' found in current text", myFixture.getCaretOffset() > 0); + } + + /** 检查自动补全所选中的第一个补全项是否与预期相符 */ + 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); + + List items = lookup.getItems(); + assertFalse("No completion items", items.isEmpty()); + + // 选择第一个补全项 + 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); + + // 模拟选中补全项 + lookup.finishLookup(Lookup.NORMAL_SELECT_CHAR); + + // 验证结果 + myFixture.checkResult(expectedText); + } + + /** + * 检查 {@link PsiElement} 的解析树是否与指定的 vfs 文件 expectedAstFile 的内容相同 + */ + protected void doAssertASTTree(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/completion/TestAutoPopupCompletion.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestAutoPopupCompletion.java index 8de3f4a1ce7cd24c99a2e217b28e22e35a0de081..3f92ecf2320a033cf091985c41c5a53f13842036 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 deleted file mode 100644 index ca08c53a796aa855794cae8215146d708ab9b41d..0000000000000000000000000000000000000000 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/completion/TestXLangCompletionContributor.java +++ /dev/null @@ -1,37 +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(); - // fixme: test the completion items - } -} 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 0000000000000000000000000000000000000000..62047e4bbb6e1fb0a70125f00645a084ed6e2d05 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/doc/TestXLangDocumentationProvider.java @@ -0,0 +1,484 @@ +/* + * 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.doc; + +import java.util.function.Consumer; + +import io.nop.idea.plugin.BaseXLangPluginTestCase; +import junit.framework.TestCase; + +/** + * 参考 https://github.com/JetBrains/intellij-community/blob/master/xml/tests/src/com/intellij/html/HtmlDocumentationTest.java + * + * @author flytreeleft + * @date 2025-06-17 + */ +public class TestXLangDocumentationProvider extends BaseXLangPluginTestCase { + + public void testGenerateDocForTag() { + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "own-tag x:schema"), // + (doc) -> { + assertTrue(doc.contains("自举定义")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + 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(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ine meta:name"), // + TestCase::assertNull // + ); + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ef:unknown-tag meta:ref"), // + (doc) -> { + assertTrue(doc.contains("所有属性和节点都必须明确声明")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "fine xdef:name"), // + (doc) -> { + assertTrue(doc.contains("定义xdef片段")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ef:prop name"), // + (doc) -> { + assertTrue(doc.contains("扩展属性")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "parse xdef:value"), // + (doc) -> { + assertTrue(doc.contains("之后执行此回调函数")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "def:unknown-tag xdsl:schema"), // + (doc) -> { + assertTrue(doc.contains("只在合并过程中存在")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "nown-tag xdef:value"), // + (doc) -> { + assertFalse(doc.contains("


")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "", // + "st-parse>"), // + (doc) -> { + assertTrue(doc.contains("xdef:post-parse")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "ef:unknown-tag"), // + (doc) -> { + assertTrue(doc.contains("Any child node")); + assertFalse(doc.contains("/test/doc/example.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "ild name"), // + (doc) -> { + assertTrue(doc.contains("This is child node")); + assertFalse(doc.contains("/test/doc/example.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/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")); + } // + ); + + // 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() { + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "xmlns:meta", // + "xmlns:meta"), // + TestCase::assertNull // + ); + + // xdef.xdef 中的 meta:xxx 属性显示相应的 xdef:xxx 属性文档 + 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(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "", // + "f=\"XDefNode\"/>"), // + (doc) -> { + assertTrue(doc.contains("引用本文件中")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "", // + "eta:ref=\"XDefNode\"/>"), // + (doc) -> { + assertTrue(doc.contains("引用本文件中")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + 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(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(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "ta:value"), // + (doc) -> { + assertTrue(doc.contains("body的数据类型")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + // *.xdef 中,xdef/x/xpl 名字空间的属性,始终显示其定义的文档 + 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(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(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "def:name=\"!var-name\""), // + (doc) -> { + assertTrue(doc.contains("注册为xdef片段")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + + 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(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(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdsl:schema", // + "xdsl:schema"), // + (doc) -> { + assertTrue(doc.contains("元模型文件路径")); + assertTrue(doc.contains("/nop/schema/xdsl.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "x:schema", // + "x:schema"), // + (doc) -> { + assertTrue(doc.contains("元模型文件路径")); + assertTrue(doc.contains("/nop/schema/xdsl.xdef")); + } // + ); + 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(insertCaretIntoVfs("/test/doc/example.xdef", // + "", // + "-attr=\"any\"/>"), // + (doc) -> { + assertTrue(doc.contains("未明确定义")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "", // + "ef:value=\"v-path-list\"/>"), // + (doc) -> { + assertTrue(doc.contains("body的数据类型")); + assertTrue(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/test/doc/example.xdef", // + "x:dump=\"true\"", // + "x:dump=\"true\""), // + (doc) -> { + assertTrue(doc.contains("是否打印合并结果")); + assertTrue(doc.contains("/nop/schema/xdsl.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/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(insertCaretIntoVfs("/nop/schema/xdef.xdef", // + "me=\"!xml-name\""), // + (doc) -> { + assertFalse(doc.contains("具有未明确定义")); + assertFalse(doc.contains("/nop/schema/xdef.xdef")); + } // + ); + assertDoc(insertCaretIntoVfs("/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")); + } // + ); + + // xlib 标签函数的参数文档 + assertDoc(""" + + + uilder="" + /> + + + """, // + (doc) -> { + assertTrue(doc.contains("Query Builder")); + assertTrue(doc.contains("for queryBuilder")); + assertTrue(doc.contains("/test/reference/a.xlib")); + } // + ); + } + + 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(""" + + + + """, // + (doc) -> { + assertTrue(doc.contains("Leaf Node")); + assertTrue(doc.contains("/dict/test/doc/child-type.dict.yaml")); + } // + ); + } + + /** 通过在 text 中插入 <caret> 代表光标位置 */ + private void assertDoc(String text, Consumer checker) { + configureByXLangText(text); + + String genDoc = getDocAtCaret(); + + checker.accept(genDoc); + } +} + 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 0000000000000000000000000000000000000000..46a7f8a71c03f847d031d0b3bdd58d029d13b71e --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangCompletions.java @@ -0,0 +1,826 @@ +/* + * 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.XLangReferenceHelper; + +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, XLangReferenceHelper.XLANG_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() { + // 对 x:prototype 属性值的补全 + // - 引用兄弟节点标签名 + assertCompletion("Get2", // + """ + + + + + + + + """, // + """ + + + + + + + + """); + // - 引用 xdef:body-type="list" 类型父节点通过 xdef:key-attr 指定的兄弟节点的唯一键属性的值 + assertCompletion("prop1", // + """ + + + + + + + + """, // + """ + + + + + + + + """); + // - 引用兄弟节点通过 xdef:unique-attr 指定的唯一键属性的值 + assertCompletion("res2", // + """ + + + + + + """, // + """ + + + + + + """); + assertCompletion("xdef:name", // + """ + + + + """, // + """ + + + + """); + + // 对 xdef:key-attr 属性值的补全 + assertCompletion("name", // + """ + + + + + + + """, // + """ + + + + + + + """); + + // 对 xdef:unique-attr/xdef:order-attr 属性值的补全 + assertCompletion("name", // + """ + + + + """, // + """ + + + + """); + assertCompletion("value", // + """ + + + + """, // + """ + + + + """); + } + + public void testAttributeValueCompletionForDefType() { + // 对 xdef-ref 引用目标的补全 + // - 引用当前 *.xdef 中的节点 + assertCompletion("Item2", // + """ + + + + + + """, // + """ + + + + + + """); + // - 引用外部 *.xdef + assertCompletion("/nop/schema/xui/xview.xdef", // + """ + + + + """, // + """ + + + + """); +// // - 引用外部 *.xdef 中的节点:TODO 对外部 xdef 中节点的引用,没有实际需求,暂不支持 +// assertCompletion("SiteResourceBean", // +// """ +// +// +// +// """, // +// """ +// +// +// +// """); + + // 对字典项/枚举项的补全 + assertCompletion("append", // + """ + + + + """, // + """ + + + + """); + assertCompletion("leaf", // + """ + + + + """, // + """ + + + + """); + +// // 对 vfs 的补全:TODO 暂不支持对文本节点的补全 +// assertCompletion("/nop/schema/xdsl.xdef", // +// """ +// +// /nop/schema/ +// +// """, // +// """ +// +// /nop/schema/xdsl.xdef +// +// """); +// assertCompletion("/test/doc/example.xdef", // +// """ +// +// /nop/schema/xdsl.xdef,/test/doc +// +// """, // +// """ +// +// /nop/schema/xdsl.xdef,/test/doc/example.xdef +// +// """); + + // 对 boolean 的补全 + assertCompletion("false", // + """ + + + + """, // + """ + + + + """); + + // 对数据域的补全 + // - 补全修饰符 + assertCompletion("!", // + """ + + + + """, // + """ + + + + """); + // - 补全数据域名字 + assertCompletion("var-name", // + """ + + + + """, // + """ + + + + """); + // - 补全 enum/dict 的选项 + assertCompletion("io.nop.xlang.xdef.XDefOverride", // + """ + + + + """, // + """ + + + + """); + assertCompletion("test/doc/child-type", // + """ + + + + """, // + """ + + + + """); + // - 补全 enum/dict 的缺省值 + assertCompletion("merge", // + """ + + + + """, // + """ + + + + """); + assertCompletion("node", // + """ + + + + """, // + """ + + + + """); + // - 补全缺省值的属性引用 + assertCompletion("path", // + """ + + + + """, // + """ + + + + """); + assertCompletion("name", // + """ + + + + """, // + """ + + + + """); + } + + public void testXlibTagCompletion() { + // - xpl:lib 导入库的补全 + assertCompletion("Query", // + """ + + + xpl:lib="/test/reference/a.xlib"/> + + + """, // + """ + + + + + + """); + // - c:import 导入库的补全 + assertCompletion("DoFindByMdxQuery", // + """ + + + + + + + """, // + """ + + + + + + + """); + + // 对标签函数参数的补全 + assertCompletion("queryBuilder", // + """ + + + + /> + + + """, // + """ + + + + + + """); + } + + /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */ + 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 new file mode 100644 index 0000000000000000000000000000000000000000..b83363c42ddd6c13593f5f45a77864b01c519842 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangParser.java @@ -0,0 +1,46 @@ +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(""" + + + /nop/schema/xdef.xdef,/nop/schema/xdsl.xdef + + /nop/schema/xdef.xdef, + /nop/schema/xdsl.xdef + + + + This is a <text/> node. + + This is a child tag. + + + + + """, // + "/test/ast/xlang-1.ast" // + ); + } + + protected void assertASTTree(String code, String expectedAstFile) { + PsiFile testFile = configureByXLangText(code); + + doAssertASTTree(testFile, 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 new file mode 100644 index 0000000000000000000000000000000000000000..1d09a58a2acc1b5705ada7e7591f4ad4756538d2 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangReferences.java @@ -0,0 +1,1244 @@ +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.PsiPackage; +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.utils.XmlPsiHelper; +import io.nop.idea.plugin.vfs.NopVirtualFile; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-06-22 + */ +public class TestXLangReferences extends BaseXLangPluginTestCase { + + public void testTagDefReferences() { + // 名字空间保持名字引用 + 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(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" // + ); + + // xdef.xdef 中的节点交叉定义 + 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(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(""" + + -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() { + // xlib 中的 source 标签内的引用 + assertReference(""" + + + + urce> + + + + """, // + "/nop/schema/xlib.xdef?source" // + ); + assertReference(""" + + + + + bc xpl:if="true"/> + + + + + """, // + "/nop/schema/xpl.xdef?xdef:unknown-tag" // + ); + +// // TODO xpl 节点内引用内置的 xpl 标签函数 +// assertReference(""" +// +// +// ort from="/test/reference/a.xlib"/> +// +// +// """, // +// "" // +// ); +// assertReference(""" +// +// +// ipt> +// +// +// """, // +// "" // +// ); + + // xlib 标签函数引用识别 + // - 通过 xpl:lib 导入 xlib + assertReference(""" + + + ByMdxQuery xpl:lib="/test/reference/a.xlib"/> + + + """, // + "/test/reference/a.xlib?DoFindByMdxQuery" // + ); + // - 通过 c:import 导入 xlib + assertReference(""" + + + + ByMdxQuery/> + + + """, // + "/test/reference/a.xlib?DoFindByMdxQuery" // + ); + assertReference(""" + + + + ByMdxQuery/> + + + """, // + "/test/reference/a.xlib?DoFindByMdxQuery" // + ); + // - thisLib 函数的识别 + assertReference(""" + + + + + mething/> + + + <_DoSomething/> + + + """, // + "_DoSomething" // + ); + // - 名字空间引用识别 + assertReference(""" + + + :DoFindByMdxQuery xpl:lib="/test/reference/a.xlib"/> + + + """, // + "a:DoFindByMdxQuery#xpl:lib=/test/reference/a.xlib" // + ); + assertReference(""" + + + + en:DoSomething/> + + + """, // + "c:import" // + ); + assertReference(""" + + + + + sLib:_DoSomething/> + + + <_DoSomething/> + + + """, // + "thisLib:_DoSomething" // + ); + + // - 标签函数中的参数识别 + assertReference(""" + + + hod="post" xpl:lib="/test/reference/a.xlib"/> + + + """, // + "/test/reference/a.xlib?attr#name=method" // + ); + assertReference(""" + + + + hod="post"/> + + + """, // + "/test/reference/a.xlib?attr#name=method" // + ); + assertReference(""" + + + + hod="post"/> + + + """, // + "/test/reference/a.xlib?attr#name=method" // + ); + } + + public void testAttributeReferences() { + // xdef.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(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(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(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(insertCaretIntoVfs("/nop/schema/xdsl.xdef", // + "xdsl:schema=", // + "xdsl:schema="), // + "/nop/schema/xdsl.xdef?xdef:unknown-tag#x:schema=v-path" // + ); + // - 普通定义 + 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(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" // + ); + assertReference(""" + + pe="leaf"/> + + """, // + "/test/doc/example.xdef?child#type=dict:test/doc/child-type=node" // + ); + assertReference(""" + + ge="22"/> + + """, // + "/test/doc/example.xdef?child#xdef:unknown-attr=any" // + ); + assertReference(""" + + ge="23"/> + + """, // + "/test/doc/example.xdef?xdef:unknown-tag#xdef:unknown-attr=any" // + ); + assertReference(""" + + ="aaa"/> + + """, // + null // + ); + + assertReference(""" + f="/test/reference/test-filter.xdef#FilterCondition" + /> + """, // + "/nop/schema/schema/schema-node.xdef?schema#ref=xdef-ref" // + ); + + // 对 Xpl 属性的引用识别 + assertReference(""" + + + mp="true"/> + + + """, // + "/nop/schema/xpl.xdef?xdef:define#xpl:dump=boolean" // + ); + 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() { + // 对 v-path 属性值的引用 + // - x:schema=v-path + assertReference(""" + + """, // + "/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" // + ); + // - 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 类型属性的引用 + // - xmlns:xxx 默认为 xdef-ref 类型 + assertReference(""" + + """, // + "/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(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(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" // + ); + assertReference(""" + + + + + """, // + null // + ); + // - 在 *.xdef 中引用外部文件 + assertReference(""" + + """, // + "/nop/schema/xdsl.xdef" // + ); + assertReference(""" + + """, // + "/nop/schema/schema/obj-schema.xdef" // + ); + // - 在 *.xmeta 中引用外部文件中的节点 + assertReference(""" + + """, // + "/test/reference/test-filter.xdef?xdef:define#xdef:name=FilterCondition" // + ); + // - 外部文件中的引用节点不存在 + assertReference(""" + + """, // + null // + ); + // - 自引用 + assertReference(""" + + + + + """, // + null // + ); + + // 对 x:prototype 属性值的引用 + 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(""" + + + + + + """, // + null // + ); + assertReference(""" + + + + + + """, // + null // + ); + // - 自引用 + assertReference(""" + + + + + + """, // + null // + ); + assertReference(""" + + + + + + """, // + null // + ); + + // 对唯一键的引用 + 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 // + ); + assertReference(""" + + + + + + """, // + null // + ); + + assertReference(""" + + + + + + """, // + "/nop/core/xlib/meta-gen.xlib" // + ); + assertReference(""" + + + + + + + + + + """, // + "/nop/core/xlib/meta-gen.xlib" // + ); + + // 非有效路径或未定义属性引用 + assertReference(insertCaretIntoVfs("/test/reference/user.view.xml", // + "xmlns:view-gen=\"view-gen\"", // + "xmlns:view-gen=\"view-gen\""), // + null // + ); + assertReference(""" + + """, // + null // + ); + + // generic-type、class-name、package-name 类型的值引用 + assertReference(""" + + + + """, // + "java.lang.String" // + ); + assertReference(""" + + + + """, // + "io.nop.xui.initialize.VueNodeStdDomainHandler" // + ); + assertReference(""" + + """, // + "io.nop.xlang.xdef" // + ); + assertReference(""" + + """, // + "io.nop.xlang.xdef.domain.XJsonDomainHandler" // + ); + assertReference(""" + + """, // + null // + ); + + // dict/enum 类型的值引用 + assertReference(insertCaretIntoVfs("/nop/schema/xlib.xdef", // + "ap\""), // + "io.nop.xlang.xdef.XDefBodyType#map" // + ); + 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" // + ); + + // 属性自引用 + assertReference(""" + + + + """, // + null // + ); + + // x:schema 指定的 *.xdef 不存在,使得 DSL 的元模型未定义,导致模型属性未知,其引用将无法识别 + // - *.xdef 不存在 + assertReference(""" + + """, // + null // + ); +// // - 属性未定义,引用无法识别:TODO 后续完成对 c:if 等内置函数的识别后,取消对普通 vfs 的文本识别 +// assertReference(""" +// +// +// +// """, // +// null // +// ); + + // 含 ${} 表达式的值,将被忽略 + assertReference(""" + + + + """, // + null // + ); + + // 对 xpl 内置函数标签的属性值的识别 + assertReference(""" + + + + + + """, // + "/test/reference/a.xlib" // + ); + assertReference(""" + + + + + + """, // + "/test/reference/a.xlib" // + ); + } + + 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\""), // + "io.nop.xlang.xdef.XDefOverride" // + ); + + // 引用字典中定义的数据域 + assertReference(""" + + + + """, // + "/dict/core/std-domain.dict.yaml#v-path" // + ); + assertReference(""" + + + + """, // + "/dict/core/std-domain.dict.yaml#string" // + ); + + // 字典/枚举的 options 引用 + assertReference(""" + + + + """, // + "/dict/test/doc/child-type.dict.yaml" // + ); + assertReference(""" + + + + """, // + 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(""" + + + + """, // + "/dict/test/doc/child-type.dict.yaml#leaf" // + ); + assertReference(""" + + + + """, // + null // + ); + assertReference(""" + + + + """, // + 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(""" + + + + """, // + "import#name=var-name" // + ); + assertReference(""" + + + + """, // + "var#type=!string" // + ); + assertReference(""" + + + + """, // + null // + ); + } + + public void testTextReferences() { + 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" // + ); + 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(); + PsiElement target = ref != null ? ref.resolve() : null; + + if (expected == null) { + assertNull(target); + return; + } + assertNotNull(target); + + assertEquals(expected, toString(target)); + } + + private String toString(@NotNull PsiElement target) { + // Note: 可能不是 vfs 文件中的元素 + String vfsPath = XmlPsiHelper.getNopVfsPath(target); + + if (target instanceof XmlTag tag) { + return (vfsPath != null ? vfsPath + '?' : "") + 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); + assert tag != null; + + return toString(tag) + '#' + attr.getName() + '=' + attr.getValue(); + } // + else if (target instanceof PsiClass cls) { + return cls.getQualifiedName(); + } // + else if (target instanceof PsiPackage pkg) { + return pkg.getQualifiedName(); + } // + else if (target instanceof PsiField field) { + PsiClass clazz = field.getContainingClass(); + assert clazz != null; + + return clazz.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()); + } + return null; + } +} 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 0000000000000000000000000000000000000000..92ce2698969e538de846dd48ae9b2197ceb85328 --- /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); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..eb378db14755999bcb3fb2f2d6302c45fb18a22b --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptCompletions.java @@ -0,0 +1,194 @@ +/** + * 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 java.util.List; + +import io.nop.idea.plugin.BaseXLangPluginTestCase; +import io.nop.idea.plugin.lang.script.XLangScriptFileType; + +/** + * @author flytreeleft + * @date 2025-06-28 + */ +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.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()); + + String expected = "import " + sample.replaceAll("\\.[^.]+$", ".") + items.get(0) + ";"; + doAssertCompletion(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(""" + 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() { + assertCompletionInXLib(""" + + + + + (); + ]]> + + + + + """, // + """ + + + + + + + + + + """ // + ); + 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(); + + + + + """ // + ); + } + + /** 需确保仅有唯一一项自动填充项:匹配是模糊匹配,需增加输入长度才能做唯一匹配 */ + protected void assertCompletion(String text, String expectedText) { + myFixture.configureByText("sample." + ext, text); + myFixture.completeBasic(); + + myFixture.checkResult(expectedText); + } + + protected void assertCompletionInXLib(String text, String expectedText) { + configureByXLangText(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 new file mode 100644 index 0000000000000000000000000000000000000000..ce761bc40c8482c93e087573e43547c7cb2e65d8 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptParser.java @@ -0,0 +1,73 @@ +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-06-28 + */ +public class TestXLangScriptParser extends BaseXLangPluginTestCase { + private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); + + public void testParseStatement() { + assertASTTree(""" + import java.lang.; + const abc = ; + const abc = () =>; + """, // + "/test/ast/xlang-script-err-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 assertASTTree(String code, String expectedAstFile) { + PsiFile testFile = myFixture.configureByText("sample." + ext, code); + + doAssertASTTree(testFile, 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 new file mode 100644 index 0000000000000000000000000000000000000000..8c878e47d1318f6bcb4d5e2b21da1e3557933935 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptReferences.java @@ -0,0 +1,424 @@ +/** + * 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.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; +import io.nop.idea.plugin.lang.script.XLangScriptFileType; +import io.nop.idea.plugin.lang.script.psi.IdentifierNode; + +/** + * @author flytreeleft + * @date 2025-06-30 + */ +public class TestXLangScriptReferences extends BaseXLangPluginTestCase { + private static final String ext = XLangScriptFileType.INSTANCE.getDefaultExtension(); + + /** 测试对导入包/类的引用 */ + public void testImportReference() { + // Note: 需确保语法完整 + + // 导入包:只能得到光标之前的已存在包 + assertReference("import io.nop.xlang.xdef;", "io.nop.xlang.xdef"); + + // 导入类 + 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(""" + 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(""" + 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(); + handler.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"; + abc = 123; + """, // + "@4" // + ); + assertReference(""" + let abc = "abc"; + abc.trim(); + """, // + "@4" // + ); + + assertReference(""" + const def = [1, 2, 3]; + def[0] = 2; + """, // + "@6" // + ); + + 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(""" + 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" // + ); + + 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(""" + 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" // + ); + } + + 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(""" + 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" // + ); + assertReference(""" + 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; } +// 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" // +// ); + } + + 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); + + 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 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 if (target instanceof IdentifierNode id) { + assertEquals(expected, "@" + id.getTextOffset()); + } // + else { + fail("Unknown target " + target); + } + } +} 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 0000000000000000000000000000000000000000..02207b7d90e2dc38942e8429a7d144842f2a9e78 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangScriptRename.java @@ -0,0 +1,447 @@ +/** + * 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 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; + }; + """ // + ); + } + + 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()); + } +} diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestMarkdownHelper.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestMarkdownHelper.java index 914bdd9e5b5896fe30b171b691a6c215674db6c9..03ed8d32084354433f0efb3a9473f1311a5618c5 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestMarkdownHelper.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestMarkdownHelper.java @@ -1,3 +1,11 @@ +/* + * 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.utils; import java.util.HashMap; 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 0000000000000000000000000000000000000000..c8e5fc4093c1374bd300eb77d4644832f33e6104 --- /dev/null +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/utils/TestXDefPsiHelper.java @@ -0,0 +1,25 @@ +/* + * 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.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/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 0000000000000000000000000000000000000000..5f3653c9b29ead772cb3e45506381a87c6a1caac --- /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/ast/xlang-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast new file mode 100644 index 0000000000000000000000000000000000000000..08a6aa6f8b0bb4df7cb9cd2f6b078d1bfd2fe3f3 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-1.ast @@ -0,0 +1,145 @@ +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:XML_TEXT + 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_CHAR_ENTITY_REF('&') + XLangValueToken:XML_ATTRIBUTE_VALUE_TOKEN(' Tag') + XmlToken:XML_ATTRIBUTE_VALUE_END_DELIMITER('"') + XmlToken:XML_EMPTY_ELEMENT_END('/>') + XLangText:XML_TEXT + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('refs') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('refs') + XmlToken:XML_TAG_END('>') + XLangText:XML_TEXT + XLangTextToken:XML_DATA_CHARACTERS('/nop/schema/xdef.xdef,/nop/schema/xdsl.xdef') + XmlToken:XML_END_TAG_START('') + XLangText:XML_TEXT + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('refs') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('refs') + XmlToken:XML_TAG_END('>') + XLangText:XML_TEXT + 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:XML_TEXT + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('text') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('text') + XmlToken:XML_TAG_END('>') + XLangText:XML_TEXT + PsiElement(XML_CDATA) + XmlToken:XML_CDATA_START('') + XmlToken:XML_END_TAG_START('') + XLangText:XML_TEXT + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('mix') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('mix') + XmlToken:XML_TAG_END('>') + 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('<') + 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:XML_TEXT + 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:XML_TEXT + PsiWhiteSpace('\n ') + XLangTag:XML_TAG('tag') + XmlToken:XML_START_TAG_START('<') + XmlToken:XML_NAME('tag') + XmlToken:XML_TAG_END('>') + XLangText:XML_TEXT + PsiElement(XML_CDATA) + XmlToken:XML_CDATA_START('') + XmlToken:XML_END_TAG_START('') + XLangText:XML_TEXT + PsiWhiteSpace('\n ') + XmlToken:XML_END_TAG_START('') + XLangText:XML_TEXT + PsiWhiteSpace('\n') + XmlToken:XML_END_TAG_START('') + PsiWhiteSpace('\n') diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-1.ast new file mode 100644 index 0000000000000000000000000000000000000000..41c2a89ba17c4311a04f764d7bb363f3bdffbdb0 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-1.ast @@ -0,0 +1,974 @@ +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') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('String') + 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)('java') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('lang') + PsiElement('.')('.') + QualifiedNameNode(qualifiedName) + RuleSpecNode(qualifiedName_name_) + IdentifierNode(identifier) + PsiElement(Identifier)('Number') + 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) + 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) + 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(',')(',') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + IdentifierNode(identifier) + PsiElement(Identifier)('mapper') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + PsiElement(SingleLineComment)('//') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + 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(',')(',') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('2') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(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(',')(',') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('2') + PsiElement(')')(')') + 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)('abc') + 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)('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) + 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(' ') + RuleSpecNode(parameterizedTypeNode) + QualifiedNameRootNode(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(' ') + RuleSpecNode(parameterizedTypeNode) + QualifiedNameRootNode(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) + 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) + 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(' ') + RuleSpecNode(parameterizedTypeNode) + QualifiedNameRootNode(qualifiedName_) + QualifiedNameNode(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') + 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(';')(';') + 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)('c') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + 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(';')(';') + 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)('def') + PsiWhiteSpace(' ') + RuleSpecNode(expression_initializer) + PsiElement('=')('=') + PsiWhiteSpace(' ') + 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('}')('}') + 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)('//') + 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) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + 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('(')('(') + 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') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + FunctionDeclarationNode(functionDeclaration) + PsiElement('function')('function') + PsiWhiteSpace(' ') + IdentifierNode(identifier) + PsiElement(Identifier)('fn2') + 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(' ') + BlockStatementNode(blockStatement) + PsiElement('{')('{') + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') + RuleSpecNode(statements_) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('const')('const') + 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(';')(';') + 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('}')('}') + PsiWhiteSpace('\n') + TopLevelStatementNode(ast_topLevelStatement) + StatementNode(statement) + FunctionDeclarationNode(functionDeclaration) + PsiElement('function')('function') + PsiWhiteSpace(' ') + IdentifierNode(identifier) + PsiElement(Identifier)('fn3') + PsiElement('(')('(') + FunctionParameterListNode(parameterList_) + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('a') + RuleSpecNode(namedTypeNode_annotation) + PsiElement(':')(':') + PsiWhiteSpace(' ') + RuleSpecNode(namedTypeNode) + TypeNameNodePredefinedNode(typeNameNode_predefined) + PsiElement('string')('string') + PsiElement(',')(',') + PsiWhiteSpace(' ') + FunctionParameterDeclarationNode(parameterDeclaration) + RuleSpecNode(ast_identifierOrPattern) + IdentifierNode(identifier) + PsiElement(Identifier)('b') + RuleSpecNode(namedTypeNode_annotation) + PsiElement(':')(':') + PsiWhiteSpace(' ') + RuleSpecNode(namedTypeNode) + 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) + PsiElement('{')('{') + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') + RuleSpecNode(statements_) + StatementNode(statement) + ReturnStatementNode(returnStatement) + PsiElement('return')('return') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + 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) + 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) + 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') + PsiElement('}')('}') + PsiWhiteSpace('\n') + PsiElement(SingleLineComment)('//') + 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(')')(')') + PsiWhiteSpace(' ') + StatementNode(statement) + BlockStatementNode(blockStatement) + PsiElement('{')('{') + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') + RuleSpecNode(statements_) + StatementNode(statement) + VariableDeclarationNode(variableDeclaration) + RuleSpecNode(varModifier_) + PsiElement('let')('let') + 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(';')(';') + PsiWhiteSpace('\n') + PsiWhiteSpace(' ') + StatementNode(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(',')(',') + PsiWhiteSpace(' ') + ExpressionNode(expression_single) + LiteralNode(literal) + RuleSpecNode(literal_numeric) + PsiElement(DecimalIntegerLiteral)('1') + PsiElement(')')(')') + RuleSpecNode(eos__) + PsiElement(';')(';') + PsiWhiteSpace('\n') + PsiElement('}')('}') + PsiWhiteSpace('\n') diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-err-1.ast b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-err-1.ast new file mode 100644 index 0000000000000000000000000000000000000000..ce1956fe130110c4950a97575b2802e8023e5e00 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/ast/xlang-script-err-1.ast @@ -0,0 +1,73 @@ +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()}? + + 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/doc/example-1.xdoc b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example-1.xdoc new file mode 100644 index 0000000000000000000000000000000000000000..8b824872fe399a22ebbcd60c83d471e446c55265 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example-1.xdoc @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..264794eedfa16d545de389943d99a4493f26068c --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdef @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..b075ebe829953e7d28067dea6eface2547423ab8 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/doc/example.xdoc @@ -0,0 +1,13 @@ + + + + /test/reference/test-filter.xdef, + /nop/schema/xdsl.xdef + + + + + + 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 0000000000000000000000000000000000000000..53618d830e01d3b4743a9772a4bd6f9276c5304b --- /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 0000000000000000000000000000000000000000..1b060f3e65a621424e1f3e4756e1d54b2930ba53 --- /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/XDefBodyType.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefBodyType.java new file mode 100644 index 0000000000000000000000000000000000000000..392a786ef5c821dcef5fbff694a7e719ca1d8645 --- /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); + } +} 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 0000000000000000000000000000000000000000..28c472b64d6b3987618c9dd26d8e31eefeb34f58 --- /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/XDefOverride.java b/nop-idea-plugin/src/test/resources/_vfs/test/java/XDefOverride.java new file mode 100644 index 0000000000000000000000000000000000000000..d8da5028b7c50f8b3cb9781066269dc04ff9c1d8 --- /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); + } +} 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 0000000000000000000000000000000000000000..c9862e1612168ed75ae2c12b7f464c1940aa7b84 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/java/XJsonDomainHandler.java @@ -0,0 +1,33 @@ +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; + } + + public static class Sub { + private String name; + public final String age; + + public String getName() { + return this.name; + } + + public static class Sub { + private String 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 new file mode 100644 index 0000000000000000000000000000000000000000..8b2ad0a8b6841f90605cfb9fa75f2141a73c6c2f --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xlib @@ -0,0 +1,101 @@ + + + + + + import io.nop.commons.util.StringHelper; + + StringHelper.escapeXml('abc'); + + + + + + + + + + + + + + This is description for queryBuilder + + + + This is description for DoFindByMdxQuery + + + a + b; + fn(1, 2); + Integer.valueOf; + Integer.valueOf('a'); + PsiClassHelper.findClass(); + scanner.col; + + 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; + let b = ``` + abcd efg + ```; + const ret = thisObj.invoke('doDeleteByQuery', { + query, authObjName, refNamesToCheck, + prepareQuery: (qry, ctx) => { + 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); + ]]> + + + + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xmeta new file mode 100644 index 0000000000000000000000000000000000000000..928def06df19a5522bf6b7d82717f0d308335298 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/a.xmeta @@ -0,0 +1,2 @@ + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/b.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/reference/b.xmeta new file mode 100644 index 0000000000000000000000000000000000000000..884f3b75d0e1d1c042843e50070989a041517cfc --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/b.xmeta @@ -0,0 +1,4 @@ + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/c.xmeta b/nop-idea-plugin/src/test/resources/_vfs/test/reference/c.xmeta new file mode 100644 index 0000000000000000000000000000000000000000..4849b2c41287c7c528dab087d4b994d540a9a130 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/c.xmeta @@ -0,0 +1,3 @@ + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/default.xform b/nop-idea-plugin/src/test/resources/_vfs/test/reference/default.xform new file mode 100644 index 0000000000000000000000000000000000000000..76dac931f7ce5a0397f6b04382704baed3871473 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/default.xform @@ -0,0 +1,2 @@ +
diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/test-filter.xdef b/nop-idea-plugin/src/test/resources/_vfs/test/reference/test-filter.xdef new file mode 100644 index 0000000000000000000000000000000000000000..4cc333d4da629358d41fbd2c86972a8bfcc84fb0 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/test-filter.xdef @@ -0,0 +1,6 @@ + + + diff --git a/nop-idea-plugin/src/test/resources/_vfs/test/reference/user.view.xml b/nop-idea-plugin/src/test/resources/_vfs/test/reference/user.view.xml new file mode 100644 index 0000000000000000000000000000000000000000..872cfdad8d1088f522e4cfe83c20e17f964b26d5 --- /dev/null +++ b/nop-idea-plugin/src/test/resources/_vfs/test/reference/user.view.xml @@ -0,0 +1,18 @@ + + + + /test/reference/a.xmeta + + + + + + + + + + + + + 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 da9f7e745343ff7d6ce7b2dbd07e5b677ec68fa2..399c9f610e447e5701448a1214a972c2340fd040 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,11 @@ 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")) + return of(ns); + } + + public static XDefKeys of(String ns) { + if (ns == null || ns.equals(DEFAULT.NS)) return DEFAULT; return new XDefKeys(ns); } @@ -187,4 +191,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 b8816679a728cf809a7b79750d396b17b5fe742e..bbaac5fc7ac62b641e8237387b9e75982e79065f 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"); @@ -111,8 +113,12 @@ 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); } -} \ No newline at end of file +}