编程语言
通过插件可以为不同的编程语言提供丰富的功能
语法高亮
VSCode 使用 TextMe 语法来定义编程语言的语法高亮。在 Github 上有多种语言的 tmbundle 的 Github 仓库,包含了基于 TextMate 语法的.tmLanguage 语法高亮定义文件。通过以下步骤,可以导入.tmlanguage 文件,为编程语言创建一个语法高亮的插件
- 在命令行中运行
yo code命令 - 选择 New Language Support 插件类型,然后输入.tmLanguage 文件的 URL 或文件路径
- 接下来根据后续选项继续创建插件项目
- 插件项目创建完成后,在视图中会显示语法高亮插件的文件结构
{
"name": "et-javascript",
"displayName": "et-javascript",
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "^1.93.0"
},
"categories": [
"Programming Languages"
],
"contributes": {
"languages": [{
"id": "javascript",
"aliases": ["javascript,typescript", "javascript"],
"extensions": [".js",".ts"],
"configuration": "./language-configuration.json"
}],
"grammars": [{
"language": "javascript",
"scopeName": "",
// 设置了描述新语言的语法的文件路径
"path": "./syntaxes/javascript.tmLanguage.json"
}]
}
}
// 语法文件, 这里采用的是TextMate语法,关于TextMate可以参考
// https://macromates.com/manual/en/language_grammars
// https://www.apeth.com/nonblog/stories/textmatebundle.html
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "javascript,typescript",
"patterns": [
{
"include": "#keywords"
},
{
"include": "#strings"
}
],
"repository": {
"keywords": {
"patterns": [{
// name是被匹配的表达式的scope selector; 关于scope selector可以参考https://macromates.com/manual/en/scope_selectors
// vscode根据这个scope selector进行上色,颜色由keyword.control.javascript控制,比如可以修改为: comment.line.double-slash.javascript,有哪些常量选择,可以参考https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json
"name": "keyword.control.javascript",
// match是一个正则表达式,但是这里使用的是ruby regular expression,进行匹配
"match": "\\b(if|while|for|return)\\b"
}]
},
"strings": {
"name": "string.quoted.double.javascript",
// 开始正则表达式匹配
"begin": "\"",
// 结束正则表达式匹配
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.javascript",
"match": "\\\\."
}
]
}
},
"scopeName": ""
}
悬停提示
- 悬停提示的是在 extension.js 中注册一个悬停事件,然后根据提供的 docuemnt、position 以及文件名,文件路径等信息作出相应的逻辑
- 悬停提示注册方法:registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable;
返回一个 HoverProvider 对象,这一对象需要加入到 context.subscription 中 - provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
这一 API 返回一个 PrioviderResult 对象,当我们把光标放在某个位置时显示的内容,就是这个对象封装的。
// extension.js的激活函数中的代码如下
// 运行插件,保证插件被激活的状态下,将光标放在json文件中的main单词上即可提示
export function activate(context: vscode.ExtensionContext) {
const hover = vscode.languages.registerHoverProvider('json', {
provideHover(document, position, token) {
const fileName = document.fileName;
const word = document.getText(document.getWordRangeAtPosition(position));
// 光标放在"main"字符时显示的内容
if (/\bmain\b/.test(word)) {
return new vscode.Hover('Hover Tip!');
}
return undefined;
},
});
context.subscriptions.push(hover);
}
代码片段
通过 contributes.snippets 贡献点,可以定义代码片段插件;
- 代码片段也叫 snippets,就是输入一个单词前缀,会根据该前缀得到一个或多个提示,然后回车键入对应的代码块
- 想要在 vscode 插件中实现 snippets 的功能,首先要在 package.json 的 contributes 配置项中配置代码提示文件的文件路径:
创建新的代码片段
通过以下步骤可以把自定义的代码片段发布成插件。
- 通过 Ctrl + Shift + P 快捷键打开命令面板,然后输入并自行
preferences: Configure User Snippets命令,可以创建一个代码片段文件,如: snippets.json - 把 snippets.json 文件复制到插件的文件夹中
- 在 package.json 文件中添加 contributes.snippets 贡献点
{
"contributes": {
"snippets": [
{
"language": "itest",
"path": "./snippets.json"
}
]
}
}
导入已有的代码片段
VSCode 支持直接导入 TextMate(.tmSnippets 文件)和 Sublime(.sublime-snippets 文件)这两种格式的代码片段。可以通过以下步骤导入 TextMate 或 Sublime 代码片段
- 在命令行中运行
yo code命令 - 选择 New Code Snippets 插件类型, 然后输入包含.tmSnippets 或.sublime-snippets 代码片段文件的文件夹
- 根据后续选项创建插件项目
- 插件项目创建完成后目录结构如下:
├── snippets
│ └── snippets.json // 代码片段的 JSON 文件
│
└── package.json // 插件清单文件
测试代码片段
测试代码片段前,需要把代码片段的插件项目复制到 VSCode 插件安装目录中。对于不同系统,安装目录有所不同:
- Windows: %USERPROFILE%.vscode\extensions
- maxOs: ~/.vscode/extensions
- Linux: ~/.vscode/extensions
复制完成后,重启 VSCode,代码片段及其插件就生效了,此时便可以对代码片段进行测试了
// package.json文件中的contributes属性-snippets
"snippets": [
{
// 表示语言何种语言(如python\javascript),这里的etest表示一个自定义的etest语言,即.etest后缀的文件;
"language": "etest",
// snippets的文件的路径
"path": "./snippets/snippets.json"
}
]
// snippets.json文件中的片段定义
{
// snippet的名称
"View组件": {
// 前缀,即输入什么字符可以出现snippets的提示
"prefix": "View",
// 回车键自动键入的代码片段(内容), 是一个数组,数组里面是字符串,每个字符串代表一行代码,${1}表示第一个光标的位置,同样,${2}表示第二个光标的位置
"body": [
"<View>",
"${1}",
"</VIew>"
],
// snippet的描述,当我们选中这个snipets提示时,描述会出现在后面
"description": "View组件"
}
}
自动补全
代码提示是我们使用 vscode 开发的时候不可或缺的重要功能,当我们输入代码的一部分的时候,vscode 会显示一个提示列表,我们可以选择其中一个提示项,按下回车后代码的剩余部分会被自动补全
代码提示相关的主要的 API:
- registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable;
- 第一个参数是实现代码提示的文件的类型。
- 第二个参数是一个 CompletionItemProvider 类型的对象,在创建这个对象内部,我们需要根据 document、position 等信息进行逻辑处理,返回一个 CompletionItem 的数组,每一个 CompletionItem 就代表一个提示项。
- 第三个参数是可选的触发提示的字符列表
下面列出一些与代码提示相关的其他的一些 API,这些 API 大多与文本、单词的处理相关,因为我们进行代码提示时需要知道当前光标所在单词的上下文,这样才能很好的给出智能提示,而要得到当前光标的上下文,就需要对光标附近乃至整个文件进行文本分析。
- 与 TextDocument 相关
TextDocument 的对象实际是当前文件对象,所以我们可以根据该对象得到当前文件与文本相关的所有信息。 - lineAt(line: number): TextLine; 根据行数返回一个行的对象
- lineAt(position: Position): TextLine; 根据一个位置返回这一行的行对象
- getText(range?: Range): string; 根据范围,返回这个范围的文本
- getWordRangeAtPosition(position: Position, regex?: RegExp): Range | undefined; 根据 position 返回这个位置所在的单词。
- text.charAt() 返回字符串在某个位置的字符
export function activate(context: vscode.ExtensionContext) {
const provider = vscode.languages.registerCompletionItemProvider('plaintext', {
provideCompletionItems(document, position) {
const completionItem1 = new vscode.CompletionItem('Hello World!');
const completionItem2 = new vscode.CompletionItem('World Peace!');
return [completionItem1, completionItem2];
},
});
// 注意:plaintext这里指的是文本文件,不是javascript, 在.txt下生效
const provider2 = vscode.languages.registerCompletionItemProvider(
'plaintext',
{
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
// get all text until the `position` and check if it reads `console.`
// and if so then complete if `log`, `warn`, and `error`
const linePrefix = document.lineAt(position).text.slice(0, position.character);
if (!linePrefix.endsWith('console.')) {
return undefined;
}
return [
new vscode.CompletionItem('log', vscode.CompletionItemKind.Method),
new vscode.CompletionItem('warn', vscode.CompletionItemKind.Method),
new vscode.CompletionItem('error', vscode.CompletionItemKind.Method),
];
},
},
'.' // triggered whenever a '.' is being typed
);
context.subscriptions.push(provider, provider2);
}
跳转到定义
跳转到定义其实很简单,通过 vscode.languages.registerDefinitionProvider 注册一个 provider,这个 provider 如果返回了 new vscode.Location()就表示当前光标所在单词支持跳转,并且跳转到对应 location
// 方法定义文件
const vscode = require('vscode');
const path = require('path');
const fs = require('fs');
/**
* 支持 package.json 中 dependencies、devDependencies 跳转到对应依赖包, 直接从 node_modules 文件夹下面去找
* 查找文件定义的provider,匹配到了就return一个location,否则不做处理
* 最终效果是,当按住Ctrl键时,如果return了一个location,字符串就会变成一个可以点击的链接,否则无任何效果
* @param {*} document
* @param {*} position
* @param {*} token
*/
function provideDefinition(document: any, position: any, token: any) {
const fileName = document.fileName;
const workDir = path.dirname(fileName);
const word = document.getText(document.getWordRangeAtPosition(position));
const line = document.lineAt(position);
const projectPath = __dirname; // util.getProjectPath(document);
// 只处理package.json文件
if (/\package\.json$/.test(fileName)) {
const json = document.getText();
if (
new RegExp(
`"(dependencies|devDependencies)":\\s*?\\{[\\s\\S]*?${word.replace(
/\//g,
'\\/'
)}[\\s\\S]*?\\}`,
'gm'
).test(json)
) {
let destPath = `${workDir}/node_modules/${word.replace(/"/g, '')}/package.json`;
if (fs.existsSync(destPath)) {
// new vscode.Position(0, 0) 表示跳转到某个文件的第一行第一列
// new vscode.Location接收2个参数,第一个是要跳转到文件的路径,第二个是跳转之后光标所在位置,接收Range或者Position对象,Position对象的初始化接收2个参数,行row和列col
return new vscode.Location(vscode.Uri.file(destPath), new vscode.Position(0, 0));
}
}
}
}
export { provideDefinition };
// extenstion.js
export function activate(context: vscode.ExtensionContext) {
// 注册如何实现跳转到定义,第一个参数表示仅对json文件生效
context.subscriptions.push(
vscode.languages.registerDefinitionProvider(['json'], {
provideDefinition,
})
);
}
语言配置
通过 contributes.languages 贡献点可以对编程语言进行配置
// language-configuration.json文件完整代码
// https://github.com/microsoft/vscode-extension-samples/blob/main/language-configuration-sample/language-configuration.json
{
"comments": {
"lineComment": "//",
"blockComment": [ "/*", "*/" ]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "`", "close": "`", "notIn": ["string", "comment"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
"autoCloseBefore": ";:.,=}])>` \n\t",
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["'", "'"],
["\"", "\""],
["`", "`"]
],
"folding": {
"markers": {
"start": "^\\s*//\\s*#?region\\b",
"end": "^\\s*//\\s*#?endregion\\b"
}
},
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
"indentationRules": {
"increaseIndentPattern": "^((?!.*?\\/\\*).*\\*\/)?\\s*[\\}\\]].*$",
"decreaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$"
}
}
- 注释
通过 comments.lineComment 和 comments.blockComment 可以定义单行注释和块注释
"comments": {
"lineComment": "//",
"blockComment": [ "/*", "*/" ]
},
- 括号匹配
当光标移动到某一括号上时,相匹配的括号也会被高亮显示,设置如下:
"brackets": [ ["{", "}"],
["[", "]"],
["(", ")"]
],
- 自动闭合
当输入 open 属性中的符号时,VSCode 会自动添加 close 属性中的符号,设置如下:
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "`", "close": "`", "notIn": ["string", "comment"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
- 自动包围
当选中代码片段并键入左括号时,VSCode 会自动添加右括号,包围选中的代码片段。如下:
// 通过surroundingPairs属性,可以定义用于包围的字符对
"surroundingPairs": [ ["{", "}"],
["[", "]"],
["(", ")"],
["'", "'"],
["\"", "\""],
["`", "`"]
],
- 代码折叠
通过 folding.markers 属性,可以定义代码折叠的正则表达式。如下:
// 设置项中将//#region和//#endregion定义为代码折叠的标记
"folding": {
"markers": {
"start": "^\\s*//\\s*#?region\\b",
"end": "^\\s*//\\s*#?endregion\\b"
}
},
- 单词的模式
// 通过wordPattern属性可以定义单词的匹配模式
{
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
}
- 缩进规则
// 通过indentationRules属性可以定义增加缩进和减少缩进的规则
"indentationRules": {
"increaseIndentPattern": "^((?!.*?\\/\\*).*\\*\/)?\\s*[\\}\\]].*$",
"decreaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$"
}
编程语言 API
通过 vscode.languages.*API ,插件可以为不同的编程语言提供非常丰富的功能。以下是一个关于悬停信息的例子
// 通过vscode.languages.registerHoverProvider API 为JavaScript文件提供了显示悬停信息的功能
vscode.languages.registerHoverProvider('javascript', {
provideHover(document, position, token) {
return {
contents: ['Hover Content'],
};
},
});
此外,我们也可以基于 Language Server Protocol(语言服务协议)实现一个 Language Server(语言服务),来为编程语言提供相应的功能。相比于直接使用 VSCode 的 vscode.languages.*API ,使用 Language Server 有以下两大好处
- Language Server 可以使用任何语言编写。比如,Java 的 Language Server 使用 Java 编写可以有更好的实现,且可以复用现有的 Java 库
- Language Server 可以被其他支持 Language Server protocol 的开发工具复用,如 VSCode、Sublime Text、Eclipse IDE 等
下表是 VSCode 的 vscode.languages.* 编程语言 API 和 Language Server Protocol 函数的对照表,开发者可根据实际情况选择相应的实现方式
| VSCode 的 vscode.language.*编程语言 API | Language Server Protocol 函数 | 描述 |
|---|---|---|
| registerCompletionItemProvider | Completion & Completion Resolve | 提供代码补齐提示 |
| registerHoverProvider | Hover | 光标停留在 token 上时触发 |
| registerSignatureHelpProvider | SignatureHelp | 提供函数签名提示 |
| 待补充 | ||