1. 从零给 VSCode 加一门语言:语法高亮到底难在哪
如果你写过自己的 DSL、配置语言,或者公司内部有一套自研脚本,大概率会遇到一个尴尬:文件在 VSCode 里打开就是一片灰白,没有颜色、没有括号配对、注释也识别不了。VSCode 插件开发里,给一门新语言做支持(language support)就是解决这个问题的核心能力,它包含语言标识注册、TextMate 语法高亮、括号匹配、注释规则、代码片段这几块。适合谁?适合已经会写基础 VSCode 扩展、想进一步扩展编辑器语言能力的开发者,也适合需要给内部语言做工具链的前端/全栈同学。
很多人第一次做会踩两个坑:一是以为语法高亮要靠写 TypeScript 逻辑,其实高亮主要靠声明式的 tmLanguage 文件;二是 package.json 里id、scopeName、language三个字段对不上,导致文件打开了但高亮完全不生效。这篇就按可复制的顺序走一遍:先注册语言贡献点,再写 tmLanguage 语法文件,然后配 language-configuration.json 做括号匹配和注释,最后用 F5 调试窗口验证高亮真的生效。中间我会顺带说下怎么用 TaoToken 统一 Key/API 通道,让 AI 帮你批量生成语法规则,省掉手写正则的重复劳动。
先明确一个概念:VSCode 的语法高亮走的是 TextMate 语法体系,它用正则把文本切成一个个 token,再给 token 打上 scope 名(比如keyword.control、string.quoted.double),主题根据 scope 上色。所以你要做两件事——告诉 VSCode「有这么一门语言」,再告诉它「这门语言的文本怎么切」。前者在 package.json 的contributes.languages,后者在contributes.grammars。理解这条主线,后面所有配置都是它的展开。
我试过把高亮逻辑写进extension.ts用装饰器实现,结果性能和主题兼容都很差,最后还是回到 tmLanguage。所以别绕路,直接按声明式配置来。
2. TaoToken 前置准备:统一 Key 与 API 通道
在动手写语法文件之前,先把 AI 辅助这条线铺好。写 tmLanguage 最烦的是正则,一门语言几十个关键字、注释、字符串、数字规则,纯手写容易漏。这时候可以让模型帮你根据语言规范生成patterns数组,你只做校对。要调模型,就需要一个稳定的 API 通道,我用的是 TaoToken。
TaoToken 是一个统一的大模型 API 接入层,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。它的价值在于:你不需要为每个模型单独维护一套 Key 和请求格式,用一个 Key 就能切换不同模型,做语法规则生成、代码片段补全、报错解释都走同一条通道。对插件开发这种需要反复试错的场景,省下的是切换成本。
前置准备分三步。第一步,去控制台创建 API Key,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,在 API Keys 页面生成,复制出来形如sk-xxxx。第二步,确认你要用的模型 ID,可以在模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 里先试聊一句,确认通道通。第三步,如果你打算长期做编码类辅助,比如让模型持续帮你补全语法规则、生成 provider 代码,可以看下 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它更适合高频调用场景。
这里要强调一个原则:TaoToken 只是模型调用通道,不替代你的编辑器,也不替代 VSCode 本身的语法解析。它帮你生成的是「配置文本」和「正则草稿」,最终生效的还是你写进插件的那几个文件。把这条边界划清楚,后面调试才不会被误导。
准备好 Key 和模型 ID 后,先做一次最小连通性验证,确认通道可用,再进入插件配置环节。验证命令在下一节给。
3. 可复制配置:package.json 语言贡献点与 tmLanguage
这一节是全文核心,所有片段都可以直接复制。先建目录结构:
my-language-support/ ├── package.json ├── language-configuration.json ├── syntaxes/ │ └── myLanguage.tmLanguage.json ├── snippets/ │ └── myLanguage.code-snippets └── src/ └── extension.ts先写package.json的贡献点。注意id、aliases、extensions、configuration四个字段要和后面文件路径严格对应:
{ "name": "my-language-support", "displayName": "My Language Support", "description": "Support for My Language", "version": "0.0.1", "engines": { "vscode": "^1.60.0" }, "categories": ["Languages"], "contributes": { "languages": [ { "id": "myLanguage", "aliases": ["My Language", "mylang"], "extensions": [".mylang"], "configuration": "./language-configuration.json" } ], "grammars": [ { "language": "myLanguage", "scopeName": "source.mylang", "path": "./syntaxes/myLanguage.tmLanguage.json" } ], "snippets": [ { "language": "myLanguage", "path": "./snippets/myLanguage.code-snippets" } ] } }三个关键点:contributes.languages[].id是语言唯一标识,后面所有 provider 注册都用它;contributes.grammars[].language必须等于这个 id;scopeName用source.前缀,主题靠它匹配。任何一处拼错,高亮都不会生效。
接着写syntaxes/myLanguage.tmLanguage.json。这是高亮的灵魂,patterns从上到下匹配,越靠前优先级越高:
{ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "name": "My Language", "scopeName": "source.mylang", "patterns": [ { "include": "#comments" }, { "include": "#keywords" }, { "include": "#strings" }, { "include": "#numbers" } ], "repository": { "comments": { "patterns": [ { "match": "//.*$", "name": "comment.line.double-slash.mylang" }, { "begin": "/\\*", "end": "\\*/", "name": "comment.block.mylang" } ] }, "keywords": { "patterns": [ { "match": "\\b(if|else|while|for|return|function|let|const)\\b", "name": "keyword.control.mylang" } ] }, "strings": { "patterns": [ { "begin": "\"", "end": "\"", "name": "string.quoted.double.mylang" } ] }, "numbers": { "patterns": [ { "match": "\\b\\d+(\\.\\d+)?\\b", "name": "constant.numeric.mylang" } ] } } }用repository+include的好处是规则可复用、可拆分,语言变复杂时不会堆成一坨。字符串用begin/end而不是单条match,是为了支持跨行和转义。
再写language-configuration.json,负责括号匹配、注释、自动闭合:
{ "comments": { "lineComment": "//", "blockComment": ["/*", "*/"] }, "brackets": [ ["{", "}"], ["[", "]"], ["(", ")"] ], "autoClosingPairs": [ { "open": "{", "close": "}" }, { "open": "[", "close": "]" }, { "open": "(", "close": ")" }, { "open": "\"", "close": "\"" } ], "surroundingPairs": [ ["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""] ] }brackets决定括号高亮配对,autoClosingPairs决定输入左括号自动补右括号,surroundingPairs决定选中文本后按括号能否包裹。这三个数组别漏,漏了就会出现「括号不亮、不自动闭合」的体感问题。
最后是代码片段snippets/myLanguage.code-snippets:
{ "Print to console": { "prefix": "print", "body": ["print(\"$1\");"], "description": "Print to the console" } }$1是光标占位符,触发后光标停在引号中间。
如果你想让 AI 帮你生成更多关键字规则,可以用 curl 走 TaoToken 通道,把语言规范丢给模型:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "你的模型ID", "messages": [ {"role": "user", "content": "给一门语言生成 TextMate tmLanguage 的 keywords patterns 数组,关键字有 if else while for return,输出 JSON"} ] }'拿到返回的patterns片段后,粘进repository.keywords即可。注意模型给的正则要自己过一遍,尤其是\b边界和转义,别直接信。
4. 验证请求与成功结果:F5 调试看高亮生效
配置写完必须验证,否则你永远不知道是文件没被加载还是正则写错。标准流程是 F5 启动扩展开发宿主(Extension Development Host)。
第一步,在插件根目录按 F5,VSCode 会新开一个窗口,标题带[Extension Development Host]。第二步,在新窗口里新建一个test.mylang文件,随便写几行:
// 这是注释 function hello() { let x = 42; print("hello"); return x; }第三步,观察结果。成功的话://那行变注释色,function、let、return变关键字色,42变数字色,"hello"变字符串色,光标放到{上时对应的}会同时高亮。第四步,输入print看是否弹出片段提示,回车后展开成print("");且光标在引号内。
如果颜色没出来,先别改正则,按这个顺序查:打开命令面板运行Developer: Inspect Editor Tokens and Scopes,把光标放到function上,看弹窗里的language是不是myLanguage、scope是不是source.mylang。如果 language 显示Plain Text,说明语言没注册成功,问题在 package.json;如果 language 对但 scope 是空的,说明 tmLanguage 没加载,问题在path或 JSON 语法。
再验证一次 AI 通道是否正常,用模型对话页发一句「解释一下 TextMate 的 begin/end 和 match 区别」,能正常返回就说明 Key 和通道没问题:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。这一步不是必须,但能帮你确认「高亮不生效」不是 AI 侧的问题。
实测下来,90% 的高亮失败都出在三个字段不一致:id、grammars.language、scopeName的对应关系。把这三个对齐,基本就通了。
5. 本篇常见错排查:401、local proxy failed、reading choices
调试过程中会遇到几类典型报错,逐个对照。
第一类,调用 TaoToken 时返回401 Unauthorized。原因通常是 Key 没带对或格式错。检查Authorization: Bearer sk-xxx里 Bearer 后面有没有空格、Key 有没有复制全、有没有多余换行。如果用的是环境变量,确认echo $TAOTOKEN_KEY能打印出来。401 是鉴权问题,跟插件本身无关,别去改 tmLanguage。
第二类,local proxy failed或连接被拒。这通常是本地网络或代理配置问题,检查你的请求地址是不是写成了https://taotoken.net/api,别多加/v1之外的路径,也别把 base 和 endpoint 拼重复。确认本机没有异常的本地代理拦截请求。
第三类,解析响应时报reading choices或cannot read property 'choices' of undefined。这说明返回体不是预期的 chat completions 结构,常见原因是模型 ID 写错、请求体 JSON 格式错、或者把messages写成了字符串。用curl -v看原始返回,确认choices[0].message.content存在。
第四类,插件侧报Cannot find module './providers/completionProvider'。这是 TS 编译路径问题,检查src目录结构和tsconfig.json的outDir,确保编译产物路径和 import 路径一致。
第五类,高亮部分生效部分不生效。比如关键字亮了但字符串不亮。这多半是patterns顺序问题——注释规则要放在最前,否则//会被当成别的 token;字符串规则要放在关键字之后,避免引号里的关键字被误匹配。调整patterns数组顺序即可。
第六类,括号不匹配高亮。检查language-configuration.json是否被contributes.languages[].configuration正确引用,路径是相对插件根目录的./language-configuration.json。文件存在但没被引用,等于没配。
把这几类对照完,基本能覆盖从通道到插件的全链路问题。遇到新报错,先定位是「AI 通道问题」还是「插件配置问题」,两边分开查,效率高很多。
6. 语义一致 CTA:把 Key、文档和编码计划串起来
配置跑通之后,你大概率会想继续扩展:加自动补全 provider、加格式化、加诊断。这些都可以让 AI 帮你起草代码,但前提是通道稳定。所以最后把入口按用途分一下,方便你按需取用。
需要创建或管理 Key,去 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入细节、请求格式、参数说明看接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。想先验证模型能力再决定用哪个,去模型对话页试:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你要长期做编码类辅助、频繁生成语法规则和 provider 代码,Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
回到插件本身,下一步最值得做的是把completionProvider和formattingProvider补上,让这门语言从「能看」变成「能写」。补全 provider 注册时记得用vscode.languages.registerCompletionItemProvider('myLanguage', provider),第一个参数就是你在 package.json 里定义的id,别写错。格式化 provider 用registerDocumentFormattingEditProvider,返回TextEdit[]。这两个 provider 的代码骨架可以让模型生成,你负责校对 API 签名和context.subscriptions.push的注册。
最后留一个实用技巧:tmLanguage 调试时,改完 JSON 不用重启整个 VSCode,在扩展开发宿主窗口按Ctrl+R重载窗口即可,比重启快得多。语法规则多起来之后,把repository按语言特性拆成多个文件用include引用,维护成本会低很多。