
Void 编辑器 Snippet 语法完全指南Tabstops、占位符、变量与变换详解【免费下载链接】void开源AI代码编辑器Cursor的替代方案。项目地址: https://gitcode.com/GitHub_Trending/void2/voidsnippet代码片段是编辑器中最常用的高效输入能力之一。本文以 src/vs/editor/contrib/snippet/browser/snippet.md 为骨架系统讲解 Void 编辑器开源的 AI 代码编辑器所采用的 TextMate 风格 snippet 模板语法涵盖 Tabstops、占位符、Choice 选择、内置变量、变量/占位符变换以及完整 EBNF 文法并结合仓库内 snippetParser.ts、snippetVariables.ts、snippetController2.ts 等源码说明其底层解析与执行原理。读完本文你将能编写任意复杂的代码片段模板理解模板中每个符号的精确语义并学会如何通过变换对插入文本做正则层面的加工。一、模板的整体心智模型一切皆 Marker在深入语法之前先建立整体认知。Void 编辑器将一段 snippet 模板解析为一棵由 Marker 组成的语法树核心节点类型定义在 snippetParser.ts 中包括Text普通文本按原样插入Placeholder占位符含 Tabstop带索引号ChoiceChoice 选项列表附着在占位符上Variable变量引用Transform附着在占位符或变量上的正则变换FormatString变换格式串中的$1、${1:...}等元素。解析入口是SnippetParser.parse()snippetParser.ts它先调用parseFragment完成语法分析再通过ensureFinalTabstop在必要处补一个$0结尾 Tabstop。整个解析过程基于一个手写的ScannersnippetParser.ts它把模板按字符逐个切分为 Token$、{、}、:、,、|、\、/、整数、变量名、普通文本等随后由_parse系列方法递归下降组装语法树。理解了这棵语法树后面所有的语法条目就都有了明确的落点。二、Tabstops控制光标移动与输入顺序原文要点使用$1、$2指定光标位置数字表示访问顺序$0表示最终光标位置。多个同号 Tabstop 是链接的会被同步更新。Tabstop 是 snippet 中最基本的导航单元${1:first} ${2:second} ${0:end}插入后光标先落在$1处按 Tab 跳至$2最后到达$0。$0是最终 Tabstopfinal tabstop到达后 snippet 会话结束。从源码看Tabstop 在语法树中就是Placeholder节点其isFinalTabstop属性判定index 0snippetParser.ts。导航动作由 snippetController2.ts 中的编辑器命令驱动jumpToNextSnippetPlaceholder主键位TabsnippetController2.tsjumpToPrevSnippetPlaceholder主键位ShiftTabsnippetController2.tsleaveSnippet主键位Esc显式退出 snippet 模式snippetController2.tsacceptSnippet直接跳完所有 Tabstop 结束会话snippetController2.ts。镜像 TabstopmirrorMultiple tabstops are linked and updated in sync指的是同一个数字出现的多次引用会形成镜像编辑其中一处其余处同步变化。这在会话层由 snippetSession.ts 的_placeholderGroups实现——按占位符索引分组移动时同一组的多个占位符同时被选中、同步编辑。末位 Tabstop 的自动补全如果你写了占位符却忘了$0编辑器会在解析时自动在末尾补一个ensureFinalTabstopsnippetParser.ts在强制末位 Tabstop或已有占位符但缺$0时向语法树末尾追加new Placeholder(0)。因此即使模板里没有$0Tab 到最后一个占位符后依然能跳回片段末尾结束。三、Placeholders带默认值的占位符原文要点占位符是带值的 Tabstop如${1:foo}插入后默认文本会被选中以便直接覆盖占位符可以嵌套如${1:another ${2:placeholder}}。${1:foo}的语义是Tabstop 1 默认填入foo光标进入时foo处于选中状态用户直接输入即可替换。占位符可以嵌套形成层级结构${1:function ${2:name}() { ${0} } }解析时嵌套占位符同样是递归的any子节点见后文 EBNF 的placeholder :: ${ int : any }。嵌套场景下的一个关键行为是默认值传播当多个占位符共享同一索引、且只有其中一个带默认值时该默认值会被克隆填充到其它同号占位符中。这一定义在SnippetParser.parseFragment中通过placeholderDefaultValues映射完成snippetParser.ts。编辑体验上的高亮由装饰decoration承担snippetSession.css 定义了.snippet-placeholder普通占位符使用--vscode-editor-snippetTabstopHighlightBackground/Border主题变量与.finish-snippet-placeholder最终占位符使用--vscode-editor-snippetFinalTabstopHighlightBackground/Border主题变量两类占位符在视觉上一眼可辨。四、Choice让用户从候选中挑选原文要点占位符的值可以是一组用逗号分隔、被管道符包裹的候选值如${1|one,two,three|}插入并选中时编辑器会提示用户在候选中选择其一。写法示例${1|true,false|} ${2|private,protected,public|} 方法的访问修饰符源码层面Choice 元素在 snippetParser.ts 中是一个独立的Choice节点其选项是若干Text。当会话进入一个带 Choice 的占位符时编辑器会注册一个临时的补全提供器snippetChoiceCompletionssnippetController2.ts把每个候选值作为一条补全项弹出选中后执行jumpToNextSnippetPlaceholder自动跳到下一 Tabstop——这就是提示用户选择的实际交互实现。五、Variables内置变量与自定义变量原文要点$name或${name:default}插入变量值。变量未设置时插入default或空字符串变量未知未定义时插入变量名本身并转化为占位符。也就是说$foo这类未定义变量不会被丢弃而是原地变成一个占位符等待用户输入这比静默输出空串更利于发现拼写错误。Void 编辑器通过 KnownSnippetVariableNames 表登记了全部内置变量名运行时由一组解析器Resolver链式求值顺序与实现位置如下变量类别变量解析器与实现位置选区相关TM_SELECTED_TEXT、TM_CURRENT_LINE、TM_CURRENT_WORD、TM_LINE_INDEX、TM_LINE_NUMBERSelectionBasedVariableResolver文档相关TM_FILENAME、TM_FILENAME_BASE、TM_DIRECTORY、TM_FILEPATH、RELATIVE_FILEPATHModelBasedVariableResolver剪贴板CLIPBOARDClipboardBasedVariableResolver注释LINE_COMMENT、BLOCK_COMMENT_START、BLOCK_COMMENT_ENDCommentBasedVariableResolver日期时间CURRENT_YEAR、CURRENT_YEAR_SHORT、CURRENT_MONTH、CURRENT_MONTH_NAME、CURRENT_MONTH_NAME_SHORT、CURRENT_DATE、CURRENT_DAY_NAME、CURRENT_DAY_NAME_SHORT、CURRENT_HOUR、CURRENT_MINUTE、CURRENT_SECOND、CURRENT_SECONDS_UNIXTimeBasedVariableResolver工作区WORKSPACE_NAME、WORKSPACE_FOLDERWorkspaceBasedVariableResolver随机值RANDOM、RANDOM_HEX、UUIDRandomBasedVariableResolver5.1 文本与选区变量原文档列出的变量在源码中均有对应实现这里给出精确语义TM_SELECTED_TEXT当前选中的文本无选区则为空字符串。多行选区时Void 会智能调整缩进比较变量所在位置的缩进与插入点缩进自动补上差额snippetVariables.ts。如果没有选中文本还会尝试使用最近一次覆盖输入overtyping的文本作为值TM_CURRENT_LINE光标所在行的完整内容TM_CURRENT_WORD光标下的单词无则空串TM_LINE_INDEX零基行号源码中为positionLineNumber - 1TM_LINE_NUMBER一基行号即直接的行号TM_FILENAME当前文档文件名TM_FILENAME_BASE去掉扩展名的文件名——注意实现细节若文件名以.开头如.gitignorelastIndexOf(.) 0时直接返回完整文件名snippetVariables.ts不会误删点文件TM_DIRECTORY当前文档所在目录TM_FILEPATH完整文件路径RELATIVE_FILEPATH相对于当前打开的工作区/文件夹的路径CLIPBOARD剪贴板内容。额外特性当启用多光标粘贴editor.multiCursorPaste: spread且剪贴板行数与光标数一致时每个光标会分到一行snippetVariables.ts。此外源码还额外登记了SELECTION与TM_SELECTED_TEXT等价、CURSOR_INDEX零基光标序号与CURSOR_NUMBER一基光标序号三个便于多光标场景使用的变量snippetVariables.ts以及CURRENT_TIMEZONE_OFFSET输出形如08:00的时区偏移。测试用例见 snippetVariables.test.ts。5.2 日期时间变量时间变量由TimeBasedVariableResolver在插入时取一次new Date()快照snippetVariables.ts各变量的输出格式变量示例CURRENT_YEAR2026CURRENT_YEAR_SHORT26CURRENT_MONTH09两位CURRENT_MONTH_NAMESeptemberCURRENT_MONTH_NAME_SHORTSepCURRENT_DATE10两位天CURRENT_DAY_NAMEWednesdayCURRENT_DAY_NAME_SHORTWedCURRENT_HOUR00–2324 小时制两位CURRENT_MINUTE42两位CURRENT_SECOND08两位CURRENT_SECONDS_UNIXUnix 时间戳秒数其中月份/日期/时分秒均通过padStart(2, 0)补齐两位snippetVariables.ts因此CURRENT_MONTH恒为两位数字与文档一致。5.3 随机值变量RANDOM6 位随机十进制数字RANDOM_HEX6 位随机十六进制数字UUIDVersion 4 UUID。实现上分别对应Math.random().toString().slice(-6)、Math.random().toString(16).slice(-6)与generateUuid()snippetVariables.ts。5.4 变量解析的链式与降级会话插入 snippet 时会构造一个CompositeSnippetVariableResolver把上述解析器串成链逐个询问、返回第一个命中结果snippetSession.ts。这正是变量未知时变成占位符的前提链上所有解析器都返回undefined则该变量保持未解析状态最终按原文规则以占位符形式留在文本中。六、Variable-Transform对变量做正则变换原文要点变换由三部分构成——(1) 匹配变量值的正则表达式(2) 引用捕获组的格式串支持条件插入与简单改写(3) 传给正则的选项。经典示例把foo.txt变成foo。${TM_FILENAME/(.*)\..$/$1/} | | | | | | | |- 无选项 | | | | | |- 引用第一个捕获组的内容 | | | |- 匹配最终 .后缀 之前所有内容的正则 | |- 解析为文件名语法结构为${变量名/正则/格式串/选项}。该结构在解析器中对应variable :: ${ var transform }分支见 EBNF_parseTransform依次吞掉三段/分隔内容最后new RegExp(regexValue, regexOptions)构造正则并挂到变量节点上snippetParser.ts。选项沿用 JavaScript 正则标志如i忽略大小写、g全局匹配默认无选项。执行时机Variable.resolve先取变量值再调用transform.resolve(value)完成替换snippetParser.ts。七、Placeholder-Transform跳走时改写占位符文本原文要点与变量变换类似但它改写的是占位符中被插入的文本当你移动到下一个 Tabstop 时正则匹配当前文本并按格式串替换同一索引的每个占位符可各自定义独立变换基于第一个占位符的值格式与变量变换相同。示例去掉开头的下划线_transform变为transform。${1/^_(.*)/$1/} | | | |- 无选项 | | | | | |- 替换为第一个捕获组 | | | |- 匹配下划线之后所有内容的正则 | |- 占位符索引执行时机与变量变换不同占位符变换发生在离开该占位符时。会话层在OneSnippet.move中对当前占位符组逐个检查placeholder.transform取当前实际文本做transform.resolve(currentValue)然后把结果以编辑操作写回编辑器snippetSession.ts。也就是说变换作用于用户最终输入的内容而不是模板默认值这使得它非常适合做输入格式化例如把用户输入的连字符命名转为驼峰。八、格式串Format String的完整语法格式串是变换的核心Void 支持比引用捕获组更丰富的结构。解析器在 snippetParser.ts 中实现了以下形式对应 EBNF 中的format产生式语法含义源码分支$1/${1}引用第 1 个捕获组简单格式${1:/upcase}转大写shorthandName 解析${1:/downcase}转小写同上${1:/capitalize}首字母大写其余不变同上${1:/camelcase}转为驼峰首个单词小写同上${1:/pascalcase}转为帕斯卡每个单词首字母大写同上${1:if}若捕获组非空则输出ifPlus分支${1:?if:else}非空输出if否则输出elseQuestionMark分支${1:-else}/${1:else}为空则输出elseDash/ 兜底分支这些条件/改写逻辑最终汇聚在FormatString.resolvesnippetParser.tsupcase/downcase/capitalize走简单字符串操作camelcase/pascalcase先按/[a-z0-9]/gi拆词再重组snippetParser.ts、?、-则依据捕获组值的有无做条件选择。组合示例——将用户输入的 snake_case 文件基名转为 PascalCase${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}配合正则选项还能做到全局替换例如把所有连字符后的字母大写camelCase 化${1/([a-z])-([a-z])/${1}${2:/upcase}/g}九、完整文法EBNF原文给出的 EBNF 是理解整个语法体系最权威的速查表这里完整保留并补充注释any :: tabstop | placeholder | choice | variable | text tabstop :: $ int | ${ int } | ${ int transform } # ${1/regex/format/options} placeholder :: ${ int : any } # ${1:默认值}可嵌套 any choice :: ${ int | text (, text)* |} # ${1|a,b,c|} variable :: $ var | ${ var } # $name / ${name} | ${ var : any } # ${name:默认值} | ${ var transform } # ${name/regex/format/options} transform :: / regex / (format | text) / options format :: $ int | ${ int } | ${ int : /upcase | /downcase | /capitalize | /camelcase | /pascalcase } | ${ int : if } | ${ int :? if : else } | ${ int :- else } | ${ int : else } regex :: JavaScript Regular Expression value (ctor-string) options :: JavaScript Regular Expression option (ctor-options) var :: [_a-zA-Z] [_a-zA-Z0-9]* int :: [0-9] text :: .*几点需要强调的推导细节any是递归产生式因此占位符内部可以再次出现占位符、变量甚至 Choice这正是嵌套占位符与变量内嵌占位符如${1:${2:foo}}的合法性来源tabstop的第三种形态${int transform}说明 Tabstop 自身也可以带变换等价于 Placeholder-Transform 的写法如${1/^_(.*)/$1/}即可写作${1/^_(.*)/$1/}这种省略冒号的形态variable支持$name简写与${name}完整形态${name}形态可再接:默认值或变换int限定为非负整数var要求以字母或下划线开头后续可跟字母、数字、下划线——这与Scanner.isVariableCharacter的实现完全一致snippetParser.tsregex/options直接使用 JavaScriptRegExp构造参数因此正则能力等同浏览器环境的 JavaScript 正则。十、转义规则与 JSON 双重转义原文要点转义使用\反斜杠。原则对原本有语法意义的字符进行转义即可——文本中可转义$、}和\Choice 元素中可转义|、,和\变换元素中可转义/和\。另外注意在 JSON 中\本身要写成\\。规则速记表上下文可转义字符普通文本$、}、\Choice 元素内|、,、\变换元素内/、\源码中这三点分别有对应实现文本转义_parseEscaped只接受\$、\}、\\三种转义序列并还原为字面字符snippetParser.tsText.escape反向使用/\$|}|\\/g做序列化snippetParser.tsChoice 转义_parseChoiceElement支持\,、\|、\\snippetParser.tsChoice.toTextmateString反向转义|、,、\snippetParser.ts变换转义_parseTransform中正则段支持\/、格式段支持\\与\/snippetParser.ts。JSON 双重转义的实战提醒当你在用户设置文件settings.json或语言扩展的snippets/*.json中编写 snippet 时反斜杠必须写成\\。例如模板中想要字面量\$输出一个美元符号且不触发变量解析在 JSON 里应写为{ EscapeDollar: { prefix: usd, body: [价格 \\$1 元] } }上例中\\$在 JSON 解析后变成\$编辑器解析模板时按转义规则把它还原为纯文本$不会当作 Tabstop 或变量。十一、错误输入与测试保障snippet 语法的正确性由完善的单元测试保障主要测试文件包括snippetParser.test.ts覆盖 Scanner 词法切分、转义、文本、Tabstop/占位符/Choice/变量/变换的解析、asInsertText等例如测试foo${f:\\}}bar解析后得到foo}bar转义生效、${1223:foo}的词法序列等snippetVariables.test.ts验证各类变量解析器对给定模型/选区/工作区的求值结果snippetController2.test.ts 与 snippetSession.test.ts验证会话级导航、镜像占位符、多光标等交互行为。从解析器的容错设计看模板即便写错也不会导致崩溃例如_parseTransform遇到非法正则时new RegExp抛错会被捕获并返回falsesnippetParser.ts解析失败的分支会回退_backTo把内容当作文本处理保证写错了也至少能插入文本。十二、综合实战示例把上述知识串起来编写一个带日期、文件基名和输入转换的文件头注释片段/* * File : ${TM_FILENAME} * Created : ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND} * Author : ${1:your name} * Purpose : ${2:${TM_SELECTED_TEXT}} * Class : ${3/\\s(\\w)/${1:/upcase}/g} */逐项解读${TM_FILENAME}与${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ...直接插入文件名与格式化时间戳${1:your name}第一处输入默认your name选中后可覆盖${2:${TM_SELECTED_TEXT}}第二处输入默认值是当前选中文本若已选中一段代码或描述直接带入${3/\s(\w)/${1:/upcase}/g}第三处输入对用户输入做全局变换——把每个单词前的空格后的首字符转大写相当于把 hello world 变成 helloWorld模板末尾未写$0解析器会自动补上最终 TabstopTab 到底后光标落在片段末尾。十三、总结snippet 模板语法虽然看似由少量符号组成但组合起来表达力极强Tabstop 与占位符解决光标顺序移动与默认值快速覆盖Choice把枚举类输入变成可视化选择变量体系覆盖文本、选区、文件、时间、随机值等几乎所有动态信息且未定义变量自动降级为占位符变换借助 JavaScript 正则与丰富的格式串实现插入前的二次加工EBNF 文法与转义规则则保证任何复杂模板都可被准确解析。这套语法的核心实现位于 src/vs/editor/contrib/snippet/browser/ 目录解析器 snippetParser.ts、变量求值 snippetVariables.ts、会话控制 snippetSession.ts 与命令注册 snippetController2.ts。在编写自己的 snippet 时随时可以回到本文核对某一符号的精确语义或翻阅上述源码与测试确认边界行为。掌握这套语法是充分发挥 Void 编辑器代码补全与自动化输入能力的关键一步。【免费下载链接】void开源AI代码编辑器Cursor的替代方案。项目地址: https://gitcode.com/GitHub_Trending/void2/void创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考