Blog

Blog

PHODAL

领域特定语言的 IDE 支持:基础篇

过去的几个月里,在设计和开发 Fklang 时,也添加了对应的 IDE 支持。由于 Intellij IDEA 插件的开发复杂,无法用文档详细描述,只能通过代码尝试。写一篇文章来记录一下这个痛苦的过程,也成为了其中的一个选择。考虑到这些只是基础的功能,所以只介绍一下:

  • 核心:语言解析
  • 与语言的 SDK 交互
  • IDE 锚点与跳转

至于其它内容,等以后有机会再写。

核心:语法解析

对了实现对于编程语言的高亮等的支持,需要使用 IDE 提供的 SDK,再次构建语言的语法树。之所以说是 “再次” 是因为在编写语言编译器时,我们已经写过一次了。与再次构建语法树相比,编写 LSP(语言服务器)也是一个选择。考虑到 LSP 的实现比较复杂,可参考的代码也比较少,所以还是采用了 JetBrains 官方推荐的方式 Grammar Kit 来开发

Grammar-Kit,一个面向语言插件开发人员的 IntelliJ IDEA 插件。并添加 BNF 语法和 JFlex 文件编辑支持,以及解析器、PSI 代码生成器。

总的来说,在编写并不复杂,也包含了对应的 PSI Viewer 插件可以 debug,还有诸如于 Erlang、Rust、D 语言等代码可以参考。不过呢,还是存在一定的差异,诸如于 private 意味着不需要生成 AST 节点,还可以通过 implementsmixin 对 AST 节点扩展。如下代码所示:

dep_source ::= dep_node
private dep_target ::= dep_node

dep_node ::= STRING_LITERAL | IDENTIFIER
{
  implements = [
    "com.feakin.intellij.psi.ext.FkMandatoryReferenceElement"
  ]
  mixin = "com.feakin.intellij.stubs.ext.misc.FkDepNodeMixin"
  stubClass = "com.feakin.intellij.stubs.FkDepNodeStub"
  elementTypeFactory = "com.feakin.intellij.stubs.StubImplementationsKt.factory"
}

只是简单的语法高亮等功能,只需要定义好 AST 节点即可(非 private,诸如于 dep_sourcedep_node 等),而诸如于 stubClass 等则是在开始其它功能使用。有了语法解析之后,就可以做更多的功能:

  • 语法高亮 ( syntaxHighlighter其依赖于不同的 ElementTypes (如 dep_node 会生成 DEP_NODE 节点)来配置颜色:ATTRIBUTES[FkElementTypes.IDENTIFIER] = FkColors.IDENTIFIER.textAttributesKey ,也可以自己定义一些颜色。
  • Structure 视图(psiStructureViewFactory也是依赖于不同的 ElementTypes
  • ……

后续也是通过 AST 节点来进行交互。

与语言的 SDK 交互

Fklang 作为一个语言,也是提供了基本的编译等的支持。在有了基本的高亮支持后,就需要与 SDK 进行交互了,也就是:

  • runLineMarkerContributor ,配置 IDE 上的 Icon 显示,并与 IDE 进行交互
  • runConfigurationProducer ,生成对应的配置
  • programRunner,执行语言 SDK 的 runner,

诸如于在 IDEA 中的 JUnit 也使用了类似的方式配置的。

IDE 锚点与跳转

这里的跳转指的是:跳转到定义(Goto…)与查找调用处(Find Usages),也因此我们需要实现两个不同的功能。但是,对于这两个功能而言,方式都是相似的,即遍历 AST 节点找到对应的子节点。跳转到定义(Goto…)相对比较简单,不需要考虑缓存等因素。与此同时,诸如于 class 的定义比较少,所以可以通过 StubClass 来进行缓存,以提升性能。

起先,我以为 reference 是通过 plugin.xml 配置的,后来发现可以在 Grammar Kit 中的 mixin 结合 PsiPolyVariantReference 配置。如下所示:

// 定义处
contextDeclaration ::= CONTEXT_KEYWORD IDENTIFIER contextBody
{
  implements = [
    "com.feakin.intellij.psi.FkNamedElement"
    "com.feakin.intellij.psi.FkNameIdentifierOwner"
    "com.feakin.intellij.psi.ext.FkMandatoryReferenceElement"
  ]
  mixin = "com.feakin.intellij.stubs.ext.FkContextDeclarationImplMixin"
  stubClass = "com.feakin.intellij.stubs.FkContextDeclarationStub"
  elementTypeFactory = "com.feakin.intellij.stubs.StubImplementationsKt.factory"
}

// 调用处
useContextName ::= IDENTIFIER
{
  implements = [
    "com.feakin.intellij.psi.FkUseElement"
    "com.feakin.intellij.psi.ext.FkMandatoryReferenceElement"
  ]
  mixin = "com.feakin.intellij.stubs.ext.FkUseContextNameImplMixin"
}

我们需要在 FkContextDeclarationImplMixinFkUseContextNameImplMixin 中配置 getReference,如下:

abstract class FkUseContextNameImplMixin(node: ASTNode) : FkElementImpl(node), FkUseContextName {
    ...
    override fun getReference(): FkReference = FkUseContextNameReferenceImpl(this)
}

稍有不同的是在 contextDeclaration 里,我们可以定义 stubClass 来配置缓存,随后在 FkUseContextNameImplMixin 中通过 FkNamedElementIndex 来查找。

其它

还有诸如编辑配置、智能感知等功能也挺有意思的,但是并不是必需的功能。

相关的参考资源:


或许您还需要下面的文章:

关于我

Github: @phodal     微博:@phodal     知乎:@phodal    

微信公众号(Phodal)

围观我的Github Idea墙, 也许,你会遇到心仪的项目

QQ技术交流群: 321689806
comment

Feeds

RSS / Atom

最近文章

关于作者

Phodal Huang

Engineer, Consultant, Writer, Designer

ThoughtWorks 技术专家

工程师 / 咨询师 / 作家 / 设计学徒

开源深度爱好者

出版有《前端架构:从入门到微前端》、《自己动手设计物联网》、《全栈应用开发:精益实践》

联系我: h@phodal.com

微信公众号: 最新技术分享

标签