日志:
- 2017.10.11 VSCode 自 v1.17 起,其代码段引擎开始支持变量转换(variable transformations)特性,变量的值可以经过格式化处理后,再插入预定的位置。这是一个很强大的特性。另外,也不知道从什么时候开始,VSCode 开始支持 Choice 了。本次更新即旨于介绍这些新特性。
前记:今天试着用了下 Atom,发现 Atom 居然预装了 CLANG 的 snippets,而且远比 VSCode 的已有拓展「C/C++ Snippets」中的丰富!身为 VSCode 的死忠粉,我决定立马把 Atom 的 C snippets 搬到 VSCode 上来。
既然你点开了这个页面,那就说明要么你不知道 VSCode 上已有拓展「C/C++ Snippets」,要么你对这个拓展不甚满意。对于后者,本文将为你介绍如何在 VSCode 上设置 snippets,并为你提供一套可以直接用的 C 语言 snippets。
snippet[ˈsnɪpɪt],或者说「code snippet」,也即代码段,指的是能够帮助输入重复代码模式,比如循环或条件语句,的模板。通过 snippet ,我们仅仅输入一小段字符串,就可以在代码段引擎的帮助下,生成预定义的模板代码,接着我们还可以通过在预定义的光标位置之间跳转,来快速补全模板。
当然,看图更易懂。下图将 aja
补全为 JQuery 的 ajax() 方法,并通过光标的跳转,快速补全了待填键值对:
设置文件头部的一个块注释给出了设置 snippet 的格式,了解过「json」就不会对此感到奇怪。
// Place your snippets for C here. Each snippet is defined under a snippet name and has a prefix, body and
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the
// same ids are connected.
// Example:
"Print to console": {
"prefix": "log",,
"body": [
"console.log('$1');",
"$2"
],
"description": "Log output to console"
}
注:上例定义了一个名为「Print to console」的 snippet,其功能为:在输入 log 并确认后,可将原文本替换为
console.log('');
。效果预览如下:
然而以上格式只是提供了较为基础的功能,实际上 VSCode 的代码段引擎所能做的远不止这些。本文将以官方教程1为本,对此进行详实地阐述。
snippet 由三部分组成:
Print to console
。其中 body 部分可以使用特殊结构,来控制光标和要插入的文本,其支持的特性及其文法如下:
Tabstops:制表符
用「Tabstops」可以让编辑器的指针在 snippet 内跳转。使用 $1
,$2
等指定光标位置。这些数字指定了光标跳转的顺序。特别地,$0
表示最终光标位置。相同序号的「Tabstops」被链接在一起,将会同步更新,比如下列用于生成头文件封装的 snippet 被替换到编辑器上时,光标就将同时出现在所有$1
位置。
"#ifndef $1"
"#define $1"
"#end // $1"
Placeholders:占位符
「Placeholder」是带有默认值的「Tabstops」,如${1:foo}
。「placeholder」文本将被插入「Tabstops」位置,并在跳转时被全选,以方便修改。占位符还可以嵌套,例如${1:another ${2:placeholder}}
。
比如,结构体的 snippet 主体可以这样写:
struct ${1:name_t} {\n\t$2\n};
作为「Placeholder」的name_t
一方面可以提供默认的结构名称,另一方面可以作为输入的提示。
Choice:可选项
「Choice」是提供可选值的「Placeholder」。其语法为一系列用逗号隔开,并最终被两个竖线圈起来的枚举值,比如 ${1|one,two,three|}
。当光标跳转到该位置的时候,用户将会被提供多个值(one 或 two 或 three)以供选择。
Variables:变量
使用$name
或${name:default}
可以插入变量的值。 当未设置变量时,将插入其缺省值或空字符串。 当varibale
未知(即,其名称未定义)时,将插入变量的名称,并将其转换为「placeholder」。 可以使用以下「Variable」:
TM_SELECTED_TEXT
:当前选定的文本或空字符串TM_CURRENT_LINE
:当前行的内容TM_CURRENT_WORD
:光标下的单词的内容或空字符串TM_LINE_INDEX
:基于零索引的行号TM_LINE_NUMBER
:基于一索引的行号TM_FILENAME
:当前文档的文件名TM_FILENAME_BASE
当前文档的文件名(不含后缀名)TM_DIRECTORY
:当前文档的目录TM_FILEPATH
:当前文档的完整文件路径注意,这些都是变量名,不是宏,在实际使用的时候还是要加上$符的。
variable transformations:变量转换
VSCode 自 v1.17 起,其代码段引擎开始支持「variable transformations」 特性,也即变量的值可以经过格式化处理后,再插入预定的位置。这是一个很强大的特性,举个例子,我们可以用它直接生成特定格式的头文件定义宏。由于截止笔者第一次更新的时候,官方教程2仍未更新相关内容,因此笔者只得根据笔者在 VSCode 项目源码中找到的文档 3,进行推理和验证,从而补全该部分空缺。
废话不说。我们可以通过 ${var_name/regular_expression/format_string/options}
的格式来使用变量转换特性。
显然,「variable transformations」也由 4 部分构成:
var_name
:变量名regular_expression
:正则表达式format_string
:格式串options
:正则表达式匹配选项其中正则表达式的写法和匹配选项部分不在本篇博文的讲解范围之内,具体内容请分别参考 javascript 有关 RegExp(pattern [, flags])
构造函数中的pattern
及flags
参数项的说明4。
下面我们将着重介绍 format_string
这个特色部分。
根据其 EBNF 范式,我们可以知道 format_string
其实是 format
或 text
的线性组合。那么 format
和 text
分别是什么呢?
text
:也即没有任何作用的普通文本,你甚至可以在其中使用汉字。format
:格式串,分为 7 种: $sn
:表示插入匹配项${sn}
:同 $sn
${sn:/upcase}
或 ${sn:/downcase}
或 ${sn:/capitalize}
:表示将匹配项变更为「所有字母均大写/所有字母均小写/首字母大写其余小写」后,插入${sn:+if}
:表示当匹配成功时,并且捕捉括号捕捉特定序号的捕捉项成功时,在捕捉项位置插入「if」所述语句${sn:?if:else}
:表示当匹配成功,并且捕捉括号捕捉特定序号的捕捉项成功时,在捕捉项位置插入「if」所述语句;否则当匹配成功,但当捕捉括号捕捉特定序号的捕捉项失败时,在捕捉项位置插入「else」所述语句${sn:-else}
:表示当匹配成功,但当捕捉括号捕捉特定序号的捕捉项失败时,在捕捉项位置插入「else」所述语句${sn:else}
:同 ${sn:-else}
其中 format
的后三条理解起来可能比较困难。这里我们以倒数第三条为例讲解。假设我们有一个「make.c」文件,我们有这么一条 snippet:"body": "${TM_FILENAME/make.c(pp|\+\+)?/${1:?c++:clang}/}"
。整个模式串匹配成功,但是捕捉括号捕捉后缀名中的 pp 或 ++ 失败,因此判断条件在捕捉括号的位置插入捕捉失败时应插入的字符串,也即「clang」。
注:
- 其中 sn 表示捕捉项的序号
- 其中 if 表示捕捉项捕捉成功时替换的文本
- 其中 else 表示捕捉项捕失败时替换的文本
下面笔者再介绍一个简单的例子,帮助大家理解「variable transformations」。
假设有一个名为「make.c」的文件中,并且我们已经定义如下 snippet。
"#ifndef HEADER … #define … #endif":{
"prefix": "defheader",
"body": "#ifndef ${1:${TM_FILENAME/(.*)\\.C$/${1:/upcase}_H/i}} \n#define $1 \n${2:header content}\n#endif\t// $1"
}
这段 snippet 将生成下图所示代码:
其中最复杂的模式为:${1:${TM_FILENAME/(.*)\\.C$/${1:/upcase}_H/i}}
,我们将之拆解为如下五部分:
${1:...}
:嵌套的 placeholder
${TM_FILENAM/.../.../.}
:「variable transformations」中的「var_name」,表示带后缀的文件名${.../(.*)\\.C$/.../.}
:「variable transformations」中的「regular_expression」,表示匹配任意以「.C」为后缀的字符串${.../.../${1:/upcase}_H/.}}
:「variable transformations」中的「options」,表示将第一个捕捉项替换为大写的,并添加「_H」的后缀${.../.../.../i}
:「variable transformations」中的「options」,表示无视大小写官网也给出了 snippet 的 EBNF 范式的正则文法,注意,使用\
(反斜杠)转义 \$
, }
和 \
。
any ::= tabstop | placeholder | choice | variable | text ←修订 tabstop ::= '$' int | '${' int '}' placeholder ::= '${' int ':' any '}' choice ::= '${' int '|' text (',' text)* '|}' ←新增 variable ::= '$' var | '${' var }' | '${' var ':' any '}' | '${' var '/' regex '/' (format | text)+ '/' options '}' ←新增 format ::= '$' int | '${' int '}' ←新增 | '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' ←新增 | '${' 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 ::= .*
Note:
1. 其中较之本文第一次提交版本新增的文法使用「←新增」标出;
2. 其中较之本文第一次提交版本修订的文法使用「←修订」标出;
3. 截止笔者第一次更新的时候,官方教程中5有关「variable transformations」的文法仍未给出。以上文法来自 VSCode 项目源码中找到的文档6;
4. 本文所附文法与上述文档有所出入,其具体位置用「←主张」标出。根据笔者对文法解析源码 7的分析,${sn:-else}
和${sn:else}
,后者除了仅在冒号(colon)后没有任何符号(加、问和减)时生效之外,与前者没有任何不同。因此我主张二者作用一致,地位相同。应该使用表示并列的「|」而非表示串接的「 」连接;
5. 笔者的主张提交至 VSCode 仓储,并通过审核。
默认情况下 snippet 在 IntelliSense 中的显示优先级并不高,而且在 IntelliSense 中选择相应 snippet 需要按「enter」键,这对于手指短的人来说并不是什么很好的体验。所幸,VSCode 意识到了这一点,并为我们提供了改进的方式。
在 VSCode 的用户设置(「Ctrl+P」在输入框中写「user settings」后点选)中,检索代码段,然后根据提示修改,设置建议优先显示,并且可以通过「TAB」补全 snippet。
修改后设置文件中会多出这两行:
"editor.snippetSuggestions": "top",
"editor.tabCompletion": true
注:我对 Atom 的 C snippet8 作了修改,使之更适合我的习惯,若有兴致你可自行修改,反正也不难。
{
/*
// Place your snippets for C here. Each snippet is defined under a snippet name and has a prefix, body and
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the
// same ids are connected.
// Example:
"Print to console": {
"prefix": "log",,
"body": [
"console.log('$1');",
"$2"
],
"description": "Log output to console"
}
*/
"#ifndef … #define … #endif":{
"prefix": "def",
"body": "#ifndef ${1:SYMBOL}\n#define $1 ${2:value}\n#endif\t// ${1:SYMBOL}"
},
"#include <>":{
"prefix": "Inc",
"body": "#include <${1:.h}>"
},
"#include \"\"":{
"prefix": "inc",
"body": "#include \"${1:.h}\""
},
"#pragma mark":{
"prefix": "mark",
"body": "#if 0\n${1:#pragma mark -\n}#pragma mark $2\n#endif\n\n$0"
},
"main()":{
"prefix": "main",
"body": "int main(int argc, char const *argv[]) {\n\t$1\n\treturn 0;\n}"
},
"For Loop":{
"prefix": "for",
"body": "for (${1:i} = 0; ${1:i} < ${2:count}; ${1:i}${3:++}) {\n\t$4\n}"
},
"Define and For Loop":{
"prefix": "dfor",
"body": "size_t ${1:i};\nfor (${1:i} = ${2:0}; ${1:i} < ${3:count}; ${1:i}${4:++}) {\n\t$5\n}"
},
"Header Include-Guard":{
"prefix": "once",
"body": "#ifndef ${1:SYMBOL}\n#define $1\n\n${2}\n\n#endif /* end of include guard: $1 */\n"
},
"Typedef":{
"prefix": "td",
"body": "typedef ${1:int} ${2:MyCustomType};"
},
"Typedef Struct":{
"prefix": "tst",
"body": "typedef struct ${1:StructName} {\n\t$2\n}${3:MyCustomType};"
},
"Do While Loop":{
"prefix": "do",
"body": "do {\n\t$0\n} while($1);"
},
"While Loop":{
"prefix": "while",
"body": "while ($1) {\n\t$2\n}"
},
"fprintf":{
"prefix": "fprintf",
"body": "fprintf(${1:stderr}, \"${2:%s}\\\\n\", $3);$4"
},
"If Condition":{
"prefix": "if",
"body": "if ($1) {\n\t$2\n}"
},
"If Else":{
"prefix": "ife",
"body": "if ($1) {\n\t$2\n} else {\n\t$3\n}"
},
"If ElseIf":{
"prefix": "iff",
"body": "if ($1) {\n\t$2\n} else if ($3) {\n\t$4\n}"
},
"If ElseIf Else":{
"prefix": "iffe",
"body": "if ($1) {\n\t$2\n} else if ($3) {\n\t$4\n} else {\n\t$5\n}"
},
"Switch Statement":{
"prefix": "sw",
"body": "switch ($1) {\n$2default:\n\t${3:break;}\n}$0"
},
"case break":{
"prefix": "cs",
"body": "case $1:\n\t$2\n\tbreak;\n$0"
},
"printf":{
"prefix": "printf",
"body": "printf(\"${1:%s }\\n\", $2);$3"
},
"scanf":{
"prefix": "scanf",
"body": "scanf(\"${1:%s}\\n\", $2);$3"
},
"Struct":{
"prefix": "st",
"body": "struct ${1:name_t} {\n\t$2\n};"
},
"void":{
"prefix": "void",
"body": "void ${1:name}($2) {\n\t$3\n}"
},
"any function":{
"prefix": "func",
"body": "${1:int} ${2:name}($3) {\n\t$5\n\treturn ${4:0};\n}"
},
"write file":{
"prefix": "wf",
"body": "FILE *${1:fp};\n${1:fp} = fopen (\"${2:filename.txt}\",\"w\");\nif (${1:fp}!=NULL)\n{\n\tfprintf(${1:fp},\"${3:Some String\\\\n}\");\n\tfclose (${1:fp});\n}"
},
"read file":{
"prefix": "rf",
"body": "FILE *${1:fp};\n${1:fp} = fopen (\"${2:filename.txt}\",\"r\");\nif (${1:fp}!=NULL)\n{\n\tfscanf(${1:fp},\"${3:Some String\\\\n}\", ${3:&var});\n\tfclose (${1:fp});\n}",
"description": "read file opeartion including fopen, fscanf and fclose."
}
}