自定义语言实现进阶¶
自定义语言支持 介绍了开发路线。本章把官方分散在 File Type、Lexer、Parser、Syntax Highlighting、Annotator、Formatter 等页面中的实现细节串成一条工程化路径,适合从“能打开文件”推进到“像一门 IDE 原生语言”。
能力分层¶
| 层级 | 目标 | 关键 API |
|---|---|---|
| 文件识别 | IDE 知道哪些文件属于语言 | Language、LanguageFileType、com.intellij.fileType |
| 词法 | 把文本切成 token | Lexer、JFlex、FlexAdapter |
| 语法和 PSI | 建 AST/PSI,支持结构化分析 | ParserDefinition、PsiParser、PsiFileBase |
| 颜色 | Lexer 级高亮和用户配色 | SyntaxHighlighter、TextAttributesKey、ColorSettingsPage |
| 语义高亮 | 根据 PSI/语义标记错误、警告、额外颜色 | Annotator、ExternalAnnotator |
| 格式化 | Reformat Code、缩进、空格、换行 | FormattingModelBuilder、Block、SpacingBuilder |
| 代码样式 | 用户可配置格式化规则 | CodeStyleSettingsProvider、custom settings |
推进顺序不要反过来。没有稳定 PSI 时实现补全、重构和 formatter,会导致后面大量返工。
File Type 注册¶
文件类型是语言插件的入口。注册时需要让 name、language 与类实现保持一致:
<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>
实现步骤:
- 实现
FormattingModelBuilder.createModel()。 - 从
FormattingContext获取 PSI、文本范围和 Code Style Settings。 - 创建
SpacingBuilder。 - 构建覆盖整个文件的根
Block。 - 每个 Block 提供缩进、换行、对齐、spacing 和子 block。
- 返回
FormattingModelProvider.createFormattingModelForPsiFile()。
关键规则:
- Block 树必须覆盖所有非空白字符,否则 formatter 可能删除未覆盖字符之间的文本。
- 空白通常不应作为 Block 覆盖,除非你明确要保留它。
- PSI 树和 Block 树通常相似,但不要求一一对应。
- Formatter 只修改 block 之间的空白。
实现顺序建议¶
- File Type + icon。
- Lexer + SyntaxHighlighter。
- ParserDefinition + PSI Viewer 验证结构。
- Parser 错误恢复。
- Annotator + Quick Fix。
- Reference + Resolve。
- Completion。
- Find Usages / Rename。
- Formatter + Code Style。
- Stub Index 和大型项目性能优化。