跳转至

自定义语言实现进阶

自定义语言支持 介绍了开发路线。本章把官方分散在 File Type、Lexer、Parser、Syntax Highlighting、Annotator、Formatter 等页面中的实现细节串成一条工程化路径,适合从“能打开文件”推进到“像一门 IDE 原生语言”。

能力分层

层级 目标 关键 API
文件识别 IDE 知道哪些文件属于语言 LanguageLanguageFileTypecom.intellij.fileType
词法 把文本切成 token Lexer、JFlex、FlexAdapter
语法和 PSI 建 AST/PSI,支持结构化分析 ParserDefinitionPsiParserPsiFileBase
颜色 Lexer 级高亮和用户配色 SyntaxHighlighterTextAttributesKeyColorSettingsPage
语义高亮 根据 PSI/语义标记错误、警告、额外颜色 AnnotatorExternalAnnotator
格式化 Reformat Code、缩进、空格、换行 FormattingModelBuilderBlockSpacingBuilder
代码样式 用户可配置格式化规则 CodeStyleSettingsProvider、custom settings

推进顺序不要反过来。没有稳定 PSI 时实现补全、重构和 formatter,会导致后面大量返工。

File Type 注册

文件类型是语言插件的入口。注册时需要让 namelanguage 与类实现保持一致:

<extensions defaultExtensionNs="com.intellij">
  <fileType
      name="Example"
      language="Example"
      implementationClass="com.example.ExampleFileType"
      fieldName="INSTANCE"
      extensions="example;ex"/>
</extensions>

官方支持的关联方式:

属性 用途
extensions 无点号扩展名,多个用分号分隔
fileNames 精确文件名
fileNamesCaseInsensitive 大小写不敏感文件名
patterns *? 文件名模式
hashBangs hashbang 模式

文件图标是最直接的验证手段:新建对应文件,Project View 中应显示你的 LanguageFileType.getIcon()

Lexer 与 TokenSet

官方教程推荐用 JFlex 生成 Lexer,再用 FlexAdapter 适配 IntelliJ Platform Lexer API:

class ExampleLexerAdapter : FlexAdapter(ExampleLexer(null))

TokenSet 应集中到单独类,尤其是 ParserDefinition 中返回的集合,避免扩展点初始化时触发过多类加载:

object ExampleTokenSets {
    val IDENTIFIERS: TokenSet = TokenSet.create(ExampleTypes.IDENTIFIER)
    val COMMENTS: TokenSet = TokenSet.create(ExampleTypes.LINE_COMMENT, ExampleTypes.BLOCK_COMMENT)
    val STRINGS: TokenSet = TokenSet.create(ExampleTypes.STRING)
}

注意:

  • Lexer 错误 token 通常返回 TokenType.BAD_CHARACTER
  • getCommentTokens() 同时影响 TODO 扫描。
  • 字符串 token 放入 getStringLiteralElements(),有助于平台理解 literals。
  • Lexer 应覆盖所有输入,不要在未知字符上卡死。

ParserDefinition 与 PSI

注册 ParserDefinition:

<extensions defaultExtensionNs="com.intellij">
  <lang.parserDefinition
      language="Example"
      implementationClass="com.example.ExampleParserDefinition"/>
</extensions>

核心方法:

class ExampleParserDefinition : ParserDefinition {
    override fun createLexer(project: Project?): Lexer = ExampleLexerAdapter()
    override fun createParser(project: Project?): PsiParser = ExampleParser()
    override fun getFileNodeType(): IFileElementType = FILE
    override fun getWhitespaceTokens(): TokenSet = TokenSet.WHITE_SPACE
    override fun getCommentTokens(): TokenSet = ExampleTokenSets.COMMENTS
    override fun getStringLiteralElements(): TokenSet = ExampleTokenSets.STRINGS
    override fun createElement(node: ASTNode): PsiElement = ExampleTypes.Factory.createElement(node)
    override fun createFile(viewProvider: FileViewProvider): PsiFile = ExampleFile(viewProvider)
}

AST 与 PSI 的关系:

  • Lexer token 是 AST 的叶子节点。
  • Parser 用 PsiBuilder.Marker 标记结构节点。
  • ParserDefinition.createElement() 把 AST 节点包装为 PSI。
  • PsiFileBase 是自定义语言文件的常用基类。

Parser 必须消费所有 token。即使遇到语法错误,也应尽量恢复并继续解析,否则编辑器中的增量分析和错误提示会很差。

Grammar-Kit 与手写 Parser

官方推荐用 Grammar-Kit 从 BNF 生成 Parser 和 PSI 样板。适合:

  • 语法相对规则。
  • 希望快速得到 PSI 类。
  • 需要 IDE 对 .bnf 的导航和重构支持。

手写 Parser 适合:

  • 语言语法高度上下文相关。
  • 已有成熟解析器需要桥接。
  • 需要特殊恢复策略或性能控制。

如果复用 ANTLR v4 语法,可以评估 antlr4-intellij-adaptor,但仍要把结果接回 IntelliJ 的 PSI、引用、索引和 formatter 体系。

SyntaxHighlighter 与 ColorSettingsPage

Lexer 级高亮通过 SyntaxHighlighter 返回 TextAttributesKey

<extensions defaultExtensionNs="com.intellij">
  <lang.syntaxHighlighterFactory
      language="Example"
      implementationClass="com.example.ExampleSyntaxHighlighterFactory"/>
</extensions>

原则:

  • 每种可配置颜色定义一个稳定的 TextAttributesKey
  • 默认颜色应继承平台常用键,例如 keyword、string、number、comment。
  • 使用 HighlighterColors.BAD_CHARACTER 高亮非法字符。
  • 实现 ColorSettingsPage 让用户在 Settings | Editor | Color Scheme 中配置颜色。
  • 可用 Jump to Colors and Fonts 或 UI Inspector 检查实际 key。

自定义语言提供 SyntaxHighlighter 后,IDE 的 “Export to HTML” 也会自动复用同一套高亮。

Annotator 与 ExternalAnnotator

Annotator 是 PSI 级语义分析入口:

<extensions defaultExtensionNs="com.intellij">
  <annotator language="Example" implementationClass="com.example.ExampleAnnotator"/>
</extensions>

适合:

  • 未解析引用。
  • 简单类型或语义错误。
  • 额外语义颜色。
  • 附带 Quick Fix 的轻量问题。

示例:

holder.newAnnotation(HighlightSeverity.WARNING, "Unknown property")
    .range(element)
    .withFix(CreatePropertyQuickFix(element.text))
    .create()

额外颜色使用 silent annotation:

holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
    .range(element.textRange)
    .textAttributes(ExampleHighlighter.SPECIAL_NAME)
    .create()

如果分析依赖外部工具,例如 schema validator、linter、编译器,使用 ExternalAnnotator。它优先级较低,会在其他后台处理完成后运行。索引期间也要运行时,2023.3+ 可实现 DumbAware

官方特别强调:Annotator 和 Inspection 不再按单个 PsiElement 顺序串行运行,应该尽量在最靠近问题的元素上产生 highlight,避免在 PsiFile 顶层扫描全部标识符。

Formatter

Formatter 的核心是 Block 树,不是直接改字符串。注册:

<extensions defaultExtensionNs="com.intellij">
  <lang.formatter
      language="Example"
      implementationClass="com.example.ExampleFormattingModelBuilder"/>
</extensions>

实现步骤:

  1. 实现 FormattingModelBuilder.createModel()
  2. FormattingContext 获取 PSI、文本范围和 Code Style Settings。
  3. 创建 SpacingBuilder
  4. 构建覆盖整个文件的根 Block
  5. 每个 Block 提供缩进、换行、对齐、spacing 和子 block。
  6. 返回 FormattingModelProvider.createFormattingModelForPsiFile()

关键规则:

  • Block 树必须覆盖所有非空白字符,否则 formatter 可能删除未覆盖字符之间的文本。
  • 空白通常不应作为 Block 覆盖,除非你明确要保留它。
  • PSI 树和 Block 树通常相似,但不要求一一对应。
  • Formatter 只修改 block 之间的空白。

实现顺序建议

  1. File Type + icon。
  2. Lexer + SyntaxHighlighter。
  3. ParserDefinition + PSI Viewer 验证结构。
  4. Parser 错误恢复。
  5. Annotator + Quick Fix。
  6. Reference + Resolve。
  7. Completion。
  8. Find Usages / Rename。
  9. Formatter + Code Style。
  10. Stub Index 和大型项目性能优化。

参考来源