idea插件开发-自定义语言03-Parse and PSI

news2024/11/16 3:29:06

        在 IntelliJ 平台中解析文件是一个两步过程:

  1. 首先,构建抽象语法树 (AST),定义程序的结构。AST 节点由 IDE 在内部创建,具体是由类ASTNode类来创建的。 每个 AST 节点都有一个关联的元素类弄IElementType实例,元素类型由语言插件定义。AST 树的顶级节点需要有一个特殊的元素类型IFileElementType。AST 节点直接映射到基础文档中的文本范围。AST 的最底部节点匹配词法分析器返回的单个标记,而更高级别的节点匹配多个标记片段。在 AST 树的节点上执行的操作,例如插入、删除、重新排序节点等,会立即反映为对基础文档文本的更改。
  2. 其次,PSI 或程序结构接口树构建在 AST 之上,添加了用于操作特定语言结构的语义和方法。PSI 树的节点由PsiElement实现,具体是由语言插件的ParserDefinition.createElement()创建。文件的PSI树的顶级节点需要实现接口PsiFile,具体由ParserDefinition.createFile()创建;

        IntelliJ Platform提供了PsiFileBase、PsiFile、ASTWrapperPsiElement的基本实现,可参考以下示例:

ublic class PropertiesParserDefinition implements ParserDefinition {

  public static final ILightStubFileElementType FILE_ELEMENT_TYPE = new ILightStubFileElementType("properties", PropertiesLanguage.INSTANCE) {
    @Override
    public FlyweightCapableTreeStructure<LighterASTNode> parseContentsLight(ASTNode chameleon) {
      PsiElement psi = chameleon.getPsi();
      assert psi != null : "Bad chameleon: " + chameleon;

      Project project = psi.getProject();
      PsiBuilderFactory factory = PsiBuilderFactory.getInstance();
      PsiBuilder builder = factory.createBuilder(project, chameleon);
      ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(getLanguage());
      assert parserDefinition != null : this;
      PropertiesParser parser = new PropertiesParser();
      return parser.parseLight(this, builder);
    }
  };

  @Override
  @NotNull
  public Lexer createLexer(Project project) {
    return new PropertiesLexer();
  }

  @Override
  public @NotNull IFileElementType getFileNodeType() {
    return FILE_ELEMENT_TYPE;
  }

  @Override
  @NotNull
  public TokenSet getWhitespaceTokens() {
    return PropertiesTokenTypes.WHITESPACES;
  }

  @Override
  @NotNull
  public TokenSet getCommentTokens() {
    return PropertiesTokenTypes.COMMENTS;
  }

  @Override
  @NotNull
  public TokenSet getStringLiteralElements() {
    return TokenSet.EMPTY;
  }

  @Override
  @NotNull
  public PsiParser createParser(final Project project) {
    return new PropertiesParser();
  }

  @Override
  public @NotNull PsiFile createFile(@NotNull FileViewProvider viewProvider) {
    return new PropertiesFileImpl(viewProvider);
  }

  @Override
  public @NotNull SpaceRequirements spaceExistenceTypeBetweenTokens(ASTNode left, ASTNode right) {
    if (left.getElementType() == PropertiesTokenTypes.END_OF_LINE_COMMENT) {
      return SpaceRequirements.MUST_LINE_BREAK;
    }
    return SpaceRequirements.MAY;
  }

  @Override
  @NotNull
  public PsiElement createElement(ASTNode node) {
    final IElementType type = node.getElementType();
    if (type == PropertiesElementTypes.PROPERTY) {
      return new PropertyImpl(node);
    }
    else if (type == PropertiesElementTypes.PROPERTIES_LIST) {
      return new PropertiesListImpl(node);
    }
    throw new AssertionError("Alien element type [" + type + "]. Can't create Property PsiElement for that.");
  }
}

避免在初始化ParserDefinition扩展点实现时进行不必要的类加载,所有TokenSet返回值都应使用专用$Language$TokenSets类中的常量。 

一、Parse and PSI

1、实现Parse

        这块建议用Grammar-Kit或Gradle Grammar-Kit插件从语法解析器生成相应的PSI类,则不要自己生动编程。因为生成的代码中同时还提供了语法高亮、快速导航、重构等能力。

        语言插件同时也提供了一个从ParserDefinition.createParser()方法返回的PsiParser接口实例的实现。 该实例用于从词法分析器获取标记流并保存正在构建的 AST 的中间状态。解析器必须处理词法分析器返回的所有标记,直到流的末尾,换句话说,直到PsiBuilder.getTokenType()返回null,即使根据语言语法这些标记是无效的,参考示例:

public class PropertiesParser implements PsiParser {
  private static final TripleFunction<ASTNode,LighterASTNode,FlyweightCapableTreeStructure<LighterASTNode>,ThreeState>
          MATCH_BY_KEY = (oldNode, newNode, structure) -> {
            if (oldNode.getElementType() == PropertiesElementTypes.PROPERTY) {
              ASTNode oldName = oldNode.findChildByType(PropertiesTokenTypes.KEY_CHARACTERS);
              if (oldName != null) {
                CharSequence oldNameStr = oldName.getChars();
                CharSequence newNameStr = findKeyCharacters(newNode, structure);

                if (!Comparing.equal(oldNameStr, newNameStr)) {
                  return ThreeState.NO;
                }
              }
            }

            return ThreeState.UNSURE;
          };

  private static CharSequence findKeyCharacters(LighterASTNode newNode, FlyweightCapableTreeStructure<LighterASTNode> structure) {
    Ref<LighterASTNode[]> childrenRef = Ref.create(null);
    int childrenCount = structure.getChildren(newNode, childrenRef);
    LighterASTNode[] children = childrenRef.get();

    try {
      for (LighterASTNode aChildren : children) {
        if (aChildren.getTokenType() == PropertiesTokenTypes.KEY_CHARACTERS) {
          return ((LighterASTTokenNode)aChildren).getText();
        }
      }
      return null;
    }
    finally {
      structure.disposeChildren(children, childrenCount);
    }
  }


  @Override
  @NotNull
  public ASTNode parse(@NotNull IElementType root, @NotNull PsiBuilder builder) {
    doParse(root, builder);
    return builder.getTreeBuilt();
  }

  @NotNull
  public FlyweightCapableTreeStructure<LighterASTNode> parseLight(IElementType root, PsiBuilder builder) {
    doParse(root, builder);
    return builder.getLightTree();
  }

  public void doParse(IElementType root, PsiBuilder builder) {
    builder.putUserData(PsiBuilderImpl.CUSTOM_COMPARATOR, MATCH_BY_KEY);
    final PsiBuilder.Marker rootMarker = builder.mark();
    final PsiBuilder.Marker propertiesList = builder.mark();
    if(builder.eof()){
      propertiesList.setCustomEdgeTokenBinders(WhitespacesBinders.GREEDY_LEFT_BINDER, WhitespacesBinders.GREEDY_RIGHT_BINDER);
    }
    else{
      propertiesList.setCustomEdgeTokenBinders(WhitespacesBinders.GREEDY_LEFT_BINDER, null);
    }

    while (!builder.eof()) {
      Parsing.parseProperty(builder);
    }
    propertiesList.done(PropertiesElementTypes.PROPERTIES_LIST);
    rootMarker.done(root);
  }
}

        解析器通过在从词法分析器接收到的标记流中设置成对的标记( PsiBuilder.Marker实例)来工作。每对标记定义了 AST 树中单个节点的词法分析器标记的范围。如果一对标记嵌套在另一对中(在其开始之后开始并在其结束之前结束),则它成为外部对的子节点。 

        通过调用PsiBuilder.Marker.done(),标记对和从它创建的 AST 节点的元素类型在设置结束标记时指定,此外也可以在设置结束标记之前删除开始标记。drop()方法只删除一个开始标记而不影响在它之后添加的任何标记,rollbackTo()方法删除开始标记和在它之后添加的所有标记并将词法分析器位置恢复到开始标记。这些方法可用于在解析前。       

        如果需要读取完数据才知道特定位置需要多少个标记时可以使用从右到左的解析方法PsiBuilder.Marker.precede()。比如二进制表达式a+b+c需要解析为( (a+b) + c )。因此,在标记“a”的位置需要两个开始标记,但直到读取标记“c”时才知道。当解析器到达 'b' 之后的 '+' 标记时,它可以调用precede()在 'a' 位置复制开始标记,然后将其匹配的结束标记放在 'c' 之后。

空格和注释

        PsiBuilder也可以对空格和注释进行处理。ParserDefinition中的getWhitespaceTokens()getCommentTokens()可以定义空格和注释标记类型。PsiBuilder会自动忽略掉PsiParser处理的数据中相关的流信息,并并调整 AST 节点的令牌范围,以便节点中不包含前导和尾随的空白令牌。但一般不需要重写上面两个方法,使用默认实现就好,处理流程如下:

 2、实现PSI

        一般来说,为自定义语言实现 PSI 没有唯一正确的方法,插件作者可以选择最适合使用 PSI 的代码的 PSI 结构和方法集(错误分析、重构和很快),但都需要实现一个基本的的PSI实现,比如支持重命名功能(通过调用PsiNamedElement的getName()setName())。

        所有实现PSI的API都放在了com.intellij.psi.util包下面,最实用的是PsiUtilCore和PsiTreeUtil工具类。

 三、实现示例

1、定义TokenType

public class SimpleTokenType extends IElementType {

  public SimpleTokenType(@NotNull @NonNls String debugName) {
    super(debugName, SimpleLanguage.INSTANCE);
  }

  @Override
  public String toString() {
    return "SimpleTokenType." + super.toString();
  }

}

2、定义ElementType

public class SimpleElementType extends IElementType {

  public SimpleElementType(@NotNull @NonNls String debugName) {
    super(debugName, SimpleLanguage.INSTANCE);
  }

}

3、定义Grammar

        在org /intellij /sdk /language /Simple.bnf文件中定义简单语言的语法。

{
  parserClass="org.intellij.sdk.language.parser.SimpleParser"

  extends="com.intellij.extapi.psi.ASTWrapperPsiElement"

  psiClassPrefix="Simple"
  psiImplClassSuffix="Impl"
  psiPackage="org.intellij.sdk.language.psi"
  psiImplPackage="org.intellij.sdk.language.psi.impl"

  elementTypeHolderClass="org.intellij.sdk.language.psi.SimpleTypes"
  elementTypeClass="org.intellij.sdk.language.psi.SimpleElementType"
  tokenTypeClass="org.intellij.sdk.language.psi.SimpleTokenType"

  psiImplUtilClass="org.intellij.sdk.language.psi.impl.SimplePsiImplUtil"
}

simpleFile ::= item_*

private item_ ::= (property|COMMENT|CRLF)

property ::= (KEY? SEPARATOR VALUE?) | KEY {
  pin=3
  recoverWhile="recover_property"
  mixin="org.intellij.sdk.language.psi.impl.SimpleNamedElementImpl"
  implements="org.intellij.sdk.language.psi.SimpleNamedElement"
  methods=[getKey getValue getName setName getNameIdentifier getPresentation]
}

private recover_property ::= !(KEY|SEPARATOR|COMMENT)

          语法规则大致如下图所示,详细可参考GitHub - JetBrains/Grammar-Kit: Grammar files support & parser/PSI generation for IntelliJ IDEA中的说明。

 4、实现语法解析器

        这一步不需要自己编码,安装Grammar-Kit或Gradle Grammar-Kit插件后,右键选择上面的.bnf文件,会有以下三个选项,选择Generate Parser Code ,解析代码会生成在 /src/main/gen中:

         还需在build.gradle.kts文件配置下gen为源码目录:

sourceSets {
  main {
    java {
      srcDirs("src/main/gen")
    }
  }
}

5、测试运行

        在新建的test.simple类中输入一个汉字会得到提示信息,因为上面规定了此文件不接收汉字。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/807943.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot整合myabtis+mysql

一、pom.xml <!--mysql驱动包--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--springboot与JDBC整合包--><dependency><groupId>org.springframework.b…

Banana Pi BPI-P2 Zero 开源硬件物联网开发板基准测试和评论

Banana Pi P2 Zero 和 P2 Maker 是基于 Allwinner 的 H3 和 H2 芯片组的廉价主板。它们以低廉的价格提供了一些有趣的功能&#xff0c;具有很大的吸引力&#xff0c;但由于其老化的 32 位架构和平庸的软件支持而有些令人失望。BPi-P2 板最适合作为无头边缘平台&#xff0c;具有…

JAVA SE -- 第十一天

&#xff08;全部来自“韩顺平教育”&#xff09; 异常-Exception 一、异常介绍 1、基本介绍 Java语言中&#xff0c;将程序执行中发生的不正常情况为“异常”&#xff08;开发过程中的语法错误和逻辑错误不是异常&#xff09; 2、执行过程中发生的异常事件可分为两大类 …

ChatGPT与高等教育变革:价值、影响及未来发展

最近一段时间&#xff0c;ChatGPT吸引了社会各界的目光&#xff0c;它可以撰写会议通知、新闻稿、新年贺信&#xff0c;还可以作诗、写文章&#xff0c;甚至可以撰写学术论文。比尔盖茨、马斯克等知名人物纷纷为此发声&#xff0c;谷歌、百度等知名企业纷纷宣布要提供类似产品。…

Windows系统远程桌面访问统信Uos社区版Deepin系统的正确方法

文章目录 0、前言1、安装X11vnc2、安装xrdp3、在Windows中以远程桌面连接3.1、可以以xorg方式远程桌面连接3.2、以vnc方式远程桌面连接黑屏 0、前言 前段时间写了篇博文【UnRaid虚拟机安装Uos家庭版并由Windows远程桌面访问的成功流程】&#xff0c;成功实现远程桌面方式登录U…

用asp.net开发h5网页版视频播放网站,类似优酷,jellyfin,emby

之前用jellyfin开源软件搞了一个视频播放服务器,用来共享给家里人看电影和电视剧,jellyfin虽然各方面功能都很强大,但是界面和使用习惯都很不适合,于是就想着利用下班休息时间做一套自己喜欢的视频网站出来. 本来是打算直接用jellyfin的源码进行修改,源码是用C# netcore 写的服…

安全学习DAY10_HTTP数据包

文章目录 HTTP数据包![请添加图片描述](https://img-blog.csdnimg.cn/32eb72ceb2d6453b94487edb1a940a43.png)Request请求数据包结构Request请求方法&#xff08;方式&#xff09;请求头&#xff08;Header&#xff09;Response响应数据包结构Response响应数据包状态码状态码作…

SQL-每日一题【1050. 合作过至少三次的演员和导演】

题目 ActorDirector 表&#xff1a; 查询合作过至少三次的演员和导演的 id 对 (actor_id, director_id) 示例 1&#xff1a; 解题思路 1.题目要求我们查询出合作过至少三次的演员和导演的 id 对&#xff0c;我们可以 group by 两次来解决这个问题。 2.首先我们按照 actor_id 进…

Pycharm debug程序,跳转至指定循环条件/循环次数

在断点出右键&#xff0c;然后设置条件 示例 for i in range(1,100):a i 1b i 2print(a, b, i) 注意&#xff1a; 1、你应该debug断点在循环后的位置而不是循环上的位置&#xff0c;然后你就可以设置你的条件进入到指定的循环上了 2、设置条件&#xff0c;要使用等于符号…

【C++ 进阶】第 1 章:[C 语言基础] C 语言概述与数据类型

目录 一、C 语言的概述 &#xff08;1&#xff09;计算机结构组成 &#xff08;2&#xff09;计算机系统组成 &#xff08;3&#xff09;ASCII 码 &#xff08;4&#xff09;计算机中的数制及其转换 &#xff08;5&#xff09;程序与指令 &#xff08;6&#xff09;语…

Diffusion扩散模型学习3——Stable Diffusion结构解析-以图像生成图像(图生图,img2img)为例

Diffusion扩散模型学习3——Stable Diffusion结构解析-以图像生成图像&#xff08;图生图&#xff0c;img2img&#xff09;为例 学习前言源码下载地址网络构建一、什么是Stable Diffusion&#xff08;SD&#xff09;二、Stable Diffusion的组成三、img2img生成流程1、输入图片编…

Mysql操作多表查询

多表查询是指在关系型数据库中&#xff0c;通过同时查询多个数据表来检索相关数据的操作。这种查询方式通常用于需要在多个数据表中搜索和比较数据的情况&#xff0c;以获取更完整和准确的结果。 在多表查询中&#xff0c;使用联接&#xff08;join&#xff09;操作将多个表连…

【Spring框架】SpringBoot创建和使用

目录 什么是SpringBoot&#xff1f;SpringBoot优点创建SpringBootSpringBoot使用 什么是SpringBoot&#xff1f; Spring 的诞⽣是为了简化 Java 程序的开发的&#xff0c;⽽ Spring Boot 的诞⽣是为了简化 Spring 程序开发的。 SpringBoot优点 1.起步依赖(创建的时候就可以方…

day47-SSM分页

SSM分页&#xff08;增删改查登录注册&#xff09; applicationContext.xml中加入mybatis-config.xml路径 mybatis-config.xml Mapper接口 Service接口及其实现类 Mapper.xml page.jsp personDetail.jsp addPerson.jsp updatePerson.jsp login.jsp regist…

DMA传输原理与实现详解(超详细)

DMA&#xff08;Direct Memory Access&#xff0c;直接内存访问&#xff09;是一种计算机数据传输方式&#xff0c;允许外围设备直接访问系统内存&#xff0c;而无需CPU的干预。 文章目录 Part 1: DMA的工作原理配置阶段&#xff1a;数据传输阶段&#xff1a; Part 2: DMA数据…

【弹力设计篇】聊聊灾备设计、异地多活设计

单机&集群架构 对于一个高可用系统来说&#xff0c;为了提升系统的稳定性&#xff0c;需要以下常用技术服务拆分、服务冗余、限流降级、高可用架构设计、高可用运维&#xff0c;而本篇主要详细介绍下&#xff0c;高可用架构设计。容灾备份以及同城多活&#xff0c;异地多活…

Python开发之手动实现一维线性插值

Python开发之手动实现一维线性插值 1.线性插值法介绍2.手动实现线性插值3.案例一手动实现线性插值4.使用pandas的插值方法实现要求(推荐) 前言&#xff1a;主要介绍手动实现一维线性插值以及pandas里面的interpolate方法实现线性插值。 1.线性插值法介绍 线性插值法是一种简单…

MySQL中锁的简介——行级锁之 间隙锁 和 临键锁

1.间隙锁演示 2.临键锁演示 间隙锁锁住的是间隙&#xff0c;不包含对应的数据记录&#xff0c;而临键锁既会包含当前这条数据记录&#xff0c;也会锁定该数据记录之前的间隙。间隙锁的目的是防止其他事务插入间隙造成幻读现象。间隙锁是可以共存的&#xff0c;一个事务采用的间…

maven引入本地jar包的简单方式【IDEA】【SpringBoot】

前言 想必点进来看这篇文章的各位&#xff0c;都是已经习惯了Maven从中央仓库或者阿里仓库直接拉取jar包进行使用。我也是&#x1f921;&#x1f921;。 前两天遇到一个工作场景&#xff0c;对接三方平台&#xff0c;结果对方就是提供的一个jar包下载链接&#xff0c;可给我整…

sqlSugar应用表值函数

一、新建表值函数 TableIntSplit 二、新建类 var employees _sqlSugarClient.Queryable<Employees>().InnerJoin(_sqlSugarClient.SqlQueryable<TableID>("select * from dbo.TableIntSplit(ids,split)").AddParameters(new { ids "1,2", s…