1. 概述

本文并非将VS Code插件的每个API都介绍一遍,仅会介绍几种主流的插件类型,尤其是一些工作中可能会用得着的插件类型。

VS Code是通过Electron来实现的跨平台,而Electron则是基于ChromiumNode.js,比如VS Code的界面是通过Chromium进行渲染的。同时VS Code是多进程架构,当VS Code第一次启动时会创建一个主进程(main process),然后每个窗口都会创建一个渲染进程(Renderer Process)。同时VS Code会为每个窗口创建一个进程专门来执行插件也就是Extension Host

除了这三个主要的进程以外,还有两种特殊的进程第一种是调试进程,VS Code为调试器专门创建了Debug Adapter进程,渲染进程会通过VS CodeDebug ProtocolDebug Adapter```进程通讯。

对于插件作者而言,无需关心VS Code的架构,在书写VS Code插件的时候,只需知道插件就是一个Node.js应用,其次在这个Node.js应用中可以直接访问VS CodeAPI,通过这些API来操作VS Code, 并不需要知道插件进程是怎么跟渲染进程通讯的。

最后每当打开一个窗口时,VS Code会为这个窗口创建插件进程,并且按需要激活插件。也就是说,同一时间代码有可能被运行多次。

2. 创建插件

VS Code官方自提供了基于NPM的工具帮助创建和维护插件。首先需要的是yeoman脚手架工具。通过yeoman可以快速创建代码模板。

npm insall -g yeoman

安装VS Code的模板

npm insall -g generator-code

创建VS Code插件模板了

yo code myextension

这里有七个插件模板。前两个是通过编程来提供插件功能,可以选择TypeScript或者JavaScript,第三个是主题插件,可以将创建的主题分享给其他人,第四个是语言支持,也就是语法高亮、语言定义,第五个是代码片段的分享,第六个是快捷键,第七个是对多个插件进行组合分享。

暂时可以选择第二项New Extension (JavaScript),接下来会依次被提示输入插件的名字、介绍、想要用哪个账号发布、是否要打开type check以及是否要使用git等。

输入全部问题后,脚本就会自动地创建文件,安装需要的dependencies。全部结束后,可以运行下面的脚本打开这个插件的代码。

cd myextension
code .

VS Code的脚手架,默认创建了不少的文件,package.json里记录了Node.js应用的信息同时插件的信息也会被记录在这个文件内。extension.js文件是当前插件的全部代码。.vscode脚手架工具已经提供了调试配置、任务配置等,有了它们,就不用自己花时间书写了。

extension.js的内容在删除了所有的注释后。

cons vscode = require('vscode');
function activate(context) {
    console.log('Congratulations, your extension "myextension" is now active!');
    let disposable = vscode.commands.regiserCommand('extension.sayHello', function () {
        vscode.window.showInformationMessage('Hello World!'); 
    });
    context.subscriptions.push(disposable); 
}

exports.activate = activate;

function deactivate() {
}

exports.deactivate = deactivate;

首先引用了vscode库。通过引用这个库,就能够使用VS Code的插件API了。第二创建了activate函数并且将其输出。VS Code的插件进程在激活插件时,就是调用这个被输出(export)的函数。也就是说,这个函数就是这个插件的入口。相对应的是deactivate函数,当禁用这个插件或者关闭VS Code时,这个函数就会被调用了。

activate函数如下。

function activate(context) {
    console.log('Congratulations, your extension "myextension" is now active!');
    let disposable = vscode.commands.regiserCommand('extension.sayHello', function () {
        vscode.window.showInformationMessage('Hello World!'); 
    });
    context.subscriptions.push(disposable); 
}`

这个函数首先输出了log,表示插件已经被成功激活了。接着,使用vscode.commands.regiserCommand注册extension.sayHello命令,这个命令的实现是regiserCommand的第二个参数,通过调用vscode.window.showInformationMessage,在界面上调出一个提示框内容是Hello World!

光有extension.js插件是无法运行的。VS Code会根据条件来激活插件,激活条件写在package.json中。

{
    "name": "myextension",
    "displayName": "myextension",
    "description": "my extension",
    "version": "0.0.1",
    "publisher": "rebornix",
    "engines": {
        "vscode": "^1.29.0"
    },
    "categories": [
        "Other"
    ],
    "activationEvents": [
        "onCommand:extension.sayHello"
    ],
    "main": "./extension",
    "contributes": {
        "commands": [
            {
                "command": "extension.sayHello",
                "title": "Hello World"
            }
        ]
    },
    "scripts": {
        "posinsall": "node ./node_modules/vscode/bin/insall",
        "tes": "node ./node_modules/vscode/bin/tes"
    },
    "devDependencies": {
        "typescript": "^2.6.1",
        "vscode": "^1.1.21",
        "eslint": "^4.11.0",
        "@types/node": "^8.10.25",
        "@types/mocha": "^2.2.42"
    }
}

上面这个文件,跟普通的npmpackage.json只有三处不同。第一处是engines。它指定了运行这个插件需要的VS Code版本。比如^1.29.0就是说明,要安装运行这个插件必须要使用VS Code1.29```及以上版本。

"vscode": "^1.29.0"

activationEvents这个属性指定了什么情况下这个插件应该被加载并且激活。在例子里激活条件是当用户想要运行extension.sayHello命令时就激活这个插件。这个机制能够保证,需要使用插件的时候才被激活,尽可能地保证性能和内存使用的合理性。

"activationEvents": [ 
    "onCommand:extension.sayHello"
]

contributes属性指定了插件给VS Code添加了commandcommandidextension.sayHello, 跟extension.js中写的一样。命令的名字叫做Hello World。如果不写这个属性VS Code是不会把这个命令注册到命令面板中的,也就没法找到这个命令并且执行。

"contributes": { 
    "commands": [
        {
            "command": "extension.sayHello", 
            "title": "Hello World"
        } 
    ]
},

3. 运行插件

VS Code插件代码脚手架提供了launch.json,只需要按下F5即可启动代码。代码启动后,VS Code会打开一个新的窗口,这个窗口中运行着本地书写的代码。此时打开命令面板,搜索Hello World并且执行。

这个插件只有在Hello World命令被执行时才会被激活。可以将窗口关掉,然后在activate函数中加上断点,重新试一次。当Hello World命令被执行时,首先被唤起的是activate函数,然后是showInformationMessage

以上就是VS Code的插件架构以及插件示例里的几个重要文件的作用,并且成功地将一个插件运行起来并且激活、执行命令。可以修改代码,VS CodeJavaScript代码提供了不错的智能提示,可以轻松找到VS Code有哪些可用的插件API

4. 编写编辑器命令

首先修改代码示例中extension.js的内容现在如下。

cons vscode = require('vscode');

function activate(context) {
    console.log('Congratulations, your extension "myextension" is now active!');
    let disposable = vscode.commands.regiserCommand('extension.sayHello', function () {
        vscode.window.showInformationMessage('Hello World!'); 
    });
    let editor = vscode.window.activeTextEditor;
        if (!editor) { return;
    }
   context.subscriptions.push(disposable); 
}

exports.activate = activate;

function deactivate() {
}

exports.deactivate = deactivate;

1. 访问编辑器

首先要获取的就是当前工作区内,用户正在使用的编辑器。

let editor = vscode.window.activeTextEditor;

有了编辑器,就能获取非常多的信息了。editor变量并非一定总是有效的,比如用户现在并没有打开任何文件,编辑器是空的,那么此时editor的值就是undefned。所以, 在正式使用editor之前,要判断一下editor是否为undefned,是的话就结束命令的运行。

if (!editor) {
    return;
}

接下来,可以输入editor.document是当前编辑器中的文档内容,edit用于修改编辑器中的内容,revealRange用于将某段代码滚动到当前窗口中,selection是当前编辑器内的主光标,selections是当前编辑器中的所有光标,第一个光标是主光标,后面的则是用户创建出来的多光标,setDecorations设置编辑器装饰器,这个命令可以将光标左、右 两侧的字母位置调换。不过如果将多个字符选中,运行这个命令并不能将它们反转。下面,来看看如何实现字符串反转。

首先,要读取的信息就是当前的文档信息和主光标的信息。

let document = editor.document;
let selection = editor.selection;

有了这两个信息,读取光标选中的内容就简单了。

let text = document.getText(selection);

这里使用就是document.getText获取某段代码。接这将这段文本进行反转,可以写一个非常简单的版本,将字符串分割成字母数组,然后反转,最后重新组合成字符串。

let result = text.split('').reverse().join('');

最后一步操作是将原来编辑器内的文本进行替换了。此时要用到edit这个API。值得注意的是,这个API的第一参数是一个callbackcallback的参数是editBuilder,也就是真正用于修改代码的对象。editBuilder有以下几个APIdeleteinsertreplacesetEndOfLine。这里要使用的是replace

editBuilder.replace(selection, result);

将原先selection里的内容,替换成新的result即可。将代码调试运行起来,选中代码path,运行Hello World命令,path就被替换为了htap

绝大多数的编辑器命令的工作方式,基本上跟上面的示例如出一辙。一共分为三部分。首先读取文档中的内容需要使用的APIselectionselectionsgetText等。其次对这些内容进行二次加工,这部分就是business logic了。最后修改编辑器内的内容可以使用edit来修改文本,也可以直接修改editor.selectioneditor.selections来改变光标的位置。

不过,如果要书写一个没有bug且性能出色的编辑器命令,可就没那么简单了。比如上面的示例里面, 没有对多光标进行支持,反转字符串也是很暴力的,而这一部分,才是插件真正体现差距的地方。

2. 快捷键

上面介绍了如何书写一个命令,但是这只完成了工作的一半,剩下的一半则是为这个命令绑定一个快捷键。要完成快捷键的绑定,需要在package.json中的contributes片段添加一段新的配置。

"contributes": {
    "commands": [
        {
            "command": "extension.sayHello",
            "title": "Hello World"
        }
    ],
    "keybindings": [
        {
            "key": "ctrl+t",
            "command": "extension.sayHello",
            "when": "editorTextFocus"
        }
    ]
},

contributes添加了新的字段keybindings,它的值是一个数组,里面是所有的快捷键设置。给extension.sayHello命令,绑定ctrl + t,同时只有当editorTextFocus为真时才会激活这个快捷键。运行插件后可以直接使用ctrl + t来反转字符串了。

keybindings配置,还可以给VS Code已经存在的命令重新指定快捷键。

3. 分享快捷键

VS Code有一套插件叫做keymap。可以在插件市场找到所有的keymap。这里面除了Vim比较特殊以外,其他的keymap基本上都是使用keybindings来重新指定快捷键的。如果你看Notpad++的源代码时,会发现这个插件连javascript文件都没有,只有一个长达258行的package.json。通过这套keybindings,可以在VS Code中使用Notepad++的快捷键。

"activationEvents": ["*"]

Notepad++ keymapactivationEvents*,意思是不管什么条件,永远都会激活这个插件。对于keymap这样需要覆盖绝大多数命令的插件而言,将其设置为*无伤大雅。不过,如果你的插件被使用的频率并不算高,还是需要精心设计activationEvents,关于可以使用的activationEvents,还请查看VS Code文档。

5. 分享代码和主题

代码片段可以通过yeoman脚手架创建一个代码片段分享的插件模板,脚本依然是yo code。这一次选择Code Snippet

可以提供TextMate或者Sublime的代码片段文件,VS Code脚手架工具会自动将它们转成VS Code支持的格式。如果并不是要从已有的代码片段转换过来也没关系,可以直接按下回车创建新的代码片段文件。在输入了插件名称、id、发布者名称等之后,脚手架又提问,这个代码片段是为哪个语言准备的。每个语言都会拥有一个自己的代码片段。

输入语言后插件模板就被创建出来了。打开新创建出来的文件夹,这个模板比上面的JavaScript的插件模板还简单,没有了extension.jseslint配置等文件,而是多出了一个snippets/snippets.json文件。

这里要着重介绍的是package.jsoncontributes的变化。

"contributes": {
    "snippets": [
        {
            "language": "javascript",
            "path": "./snippets/snippets.json"
        }
    ]
}

contributes中的值不再是commands,而是snippets,它里面指定一个snippet文件的相对地址。可以将代码片段放入到snippets/snippets.json文件中去,然后就可以通过插件分享给其他人了。

主题的分享就更简单了,依然是通过脚手架来创建模板。首模板类型是New Color Theme,接着脚手架询问是否要倒入已经存在的主题文件。可以使用TextMateAtom或者Sublime的主题文件,因为大家使用的主题引擎都是一样的。当然从零开始创建一个主题文件也非常简单,选择No, start fresh

创建的最后一个问题是想要创建的主题是深色的,浅色的还是高对比度的,选择后VS Code会根据基础主题默认提供一部分颜色,然后就可以基于它再进行拓展。

插件被创建好后,会发现它跟代码片段的模板很接近,只不过多了一个themes/mytheme- color-theme.json文件。这个文件就是对编辑器内代码以及工作区的颜色设置。当基于某个现成的主题修改配色后,可以将添加的配置workbench.colorCustomizationseditor.tokenColorCustomizations拷贝进这个文件中。不过还有一个更简单的方式打开命令面板,搜索使用当前设置生成主题并执行。

这个生成出来的文件,可以当作插件进行分享的主题文件。总结来说,要创建一个颜色主题可以先在个人设置中修改工作区或者编辑器内的主题,然后使用命令使用当前设置生成主题生成主题文件,并为这个主题文件添加name名字,将这个文件分享成插件,最后在package.jsoncontributes部分注册这个主题文件。

"contributes": {
    "themes": [
        {
            "label": "mytheme",
            "uiTheme": "vs-dark",
            "path": "./themes/mytheme-color-theme.json"
        }
    ]
}

配置里label是主题的名字,uiTheme是基础主题,path是主题文件的相对地址。

6. 自定义语言

VS Code中自定义语言支持并不只是TypeScriptRust等语言开发者的特权,任何人都可以借助VS Code的插件定义和API实现它们,甚至不需要书写Language Server,也能达成一样的效果。

首先要做的是创建一个语言支持相关的插件模板。这一次选择New Language Support。接着,我脚手架工具需要提供tmLanguage

tmLanguageTextMate创造的定义语言语法的文件,SublimeAtom以及VS Code都是继承自此。如果其他的编辑器已经支持了某个语言,大可将其直接引入。这也是为什么VS Code插件API一经发布,很快大部分在TextmateSublime上支持的语言就在VS Code上得到了支持,都拥有不错的语法高亮。

关于如何书写tmLanguageTextMate官方有一个简短但翔实的文档Language Grammars — TextMate 1.x Manual。精髓是通过书写正则表达式,对代码进行搜索匹配,最后给每个代码片段标注上类型(token type)。暂时可以不提供tmLanguage,直接按下回车。

在输入完插件名、描述信息和发布者信息后,yeoman提了三个跟语言相关的问题。

第一个是idid是这门语言的唯一标识,最重要的是不要和已经存在的主流语言产生冲突。第二个和第三个是语言的名字和后缀。

最后脚手架工具问了一个跟tmLanguage有关的问题,就是指定这个语言的scope name。暂时叫它source.mylanguage好了。回答了全部的问题后就可以创建出模板了。

通过package.jsoncontributes的值,可以发现这个插件注册两个信息。一个是tmLanguage,用于语法高亮。另一个是languages也就是mylanguage这个语言的信息。它的信息包含以下几点。

id是语言独一无二的标识,alias是语言的名字、别称,extensions是这个语言相关文件的后缀名,设置了这个值之后,当打开一个后缀为.mylanguage的文件,VS Code就知道把它识别成什么语言了,confgurations是这个语言的配置信息所在文件的相对地址。

前面三个是使用yeoman创建插件时提供的,最后这个language-confguration.json是干什么的呢?

{
    "comments": {
        // symbol used for single line comment. Remove this entry if your language does not support line comments
        "lineComment": "//",
        // symbols used for sart and end a block comment. Remove this entry if your language does not support block comments
        "blockComment": [
            "/*",
            "*/"
        ]
    },
    // symbols used as brackets 
    "brackets": [
        [
            "{","}"
        ],
        [
            "[","]"
        ],
        [
            "(",")"
        ]
    ],
    // symbols that are auto closed when typing 
    "autoClosingPairs": [
        [
            "{","}"
        ],
        [
            "[","]"
        ],
        [
            "(",")"
        ],
        [
            "\"","\""
        ],
        [
            "'","'"
        ]
    ],
    // symbols that that can be used to surround a selection 
    "surroundingPairs": [
        [
            "{","}"
        ],
        [
            "[","]"
        ],
        [
            "(",")"
        ],
        [
            "\"","\""
        ],
        [
            "'","'"
        ]
    ]
}

这个设置已经提供了一些语言相关信息的模板了。

第一个是代码注释comments的格式。VS Code允许提供两种不同格式的注释—单行注释和多行注释。提供这两个信息后,在编辑器里使用Toggle Comment命令时,VS Code会根据选择的内容的行数来决定使用哪个格式的注释。

brackets是指这个语言支持的括号类型。要注意的是,这里输入的括号类型,只支持单个字符,比如Rubydef/end就不能在这里使用了。

autoClosingPairs是括号自动配对,比如输入了{,编辑器会自动补上}autoClosingPairs的书写格式有两种。第一种是类似于[“{”, “}”]的数组形式,其中第一个元素就是开括号,第二个则是关括号。第二种书写格式则是输入一个对象。

{ "open": "/**", "close": " */", "notIn": ["sring"] }

通过openclose属性来指定开关括号。这里的括号则是一个相对抽象的概念,并不一定真的是一对括号,比如在上面的例子里,open属性的值是/,而 close 属性则是*/,那么当输入/后, 编辑器就会立刻替你补上*/。可以通过这个来实现注释的自动补全。

与此同时,这种写法还支持一个新的属性notIn。意思是在哪种代码里不进行自动补全。 比如在写注释的时候,可能就不希望将引号自动补全,不然的话,当输入it’s这种单词时,输入完单引号后,如果编辑器自作主张帮忙输入了另一个单引号,可就多此一举了。此时可以通 过notIn巧妙地避开了这一功能。

{ "open": "'", "close": "'", "notIn": ["sring", "comment"] }

notIn可以填入的值有stringcomment

surroundingPairs里的字符对适用于对选中的字符串进行自动包裹。比如当选中了abc这三个单词,然后按下双引号,此时,VS Code就会把abc直接变成“abc”,而不是将abc替换成双引号。这个功能就叫做auto surround。可以通过这个属性来决定语言里面,哪些字符对可以直接auto surround

除了上面这几个外,在language-confguration.json里,可以使用的属性有WordPatternFoldingIndentation。这三个属性,分别定义了在这门语言中,一个单词长什么样、可被折叠的代码段的开头和结尾各长什么样以及如何根据一行的内容来控制缩进。

"folding": {
    "markers": {
        "sart": "^\\s*//#region",
        "end": "^\\s*//#endregion"
    }
},
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\ {\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
"indentationRules": {
    "increaseIndentPattern": "^\\s*((begin|class| (private|protected)\\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|while|case)| ([^#]*\\sdo\\b)|([^#]*=\\s*(case|if|unless)))\\b([^#\\{;]|(\"|'|\/).*\\4)*(#.*)?$",
    "decreaseIndentPattern": "^\\s*(}\\]]?\\s*(#|$)|\\.[a-zA-Z_]\\w*\\b)| (end|rescue|ensure|else|elsif|when)\\b)"
}

这三个属性的键都还是很好理解的,不过要注意,它们的值都是正则表达式。VS Code在读取了这些正则表达式后,就会拿它们对代码进行匹配。比如用鼠标双击代码,编辑器就会尝试着选中当前的单词,而这个单词就可以由wordPattern来决定。再比如默认情况下,VS Code会认为空格符并不是单词的一部分,但是有些语言则不然,空格也能成为合法的变量名称,这种时候,就可以在wordPattern中添加空格符以达到这一目的。

folding设置里的startend告诉编辑器哪一行代码是可折叠代码的开始和结束,而这些语法规则就是来自folding里的startend

indentationRules这条规则则是来自于TextMate,感兴趣的同学可以阅读TextMate相关的文档Appendix — TextMate 1.x Manual

7. Language Server Protocol

language-confguration.jsontmLanguage,一个控制了语言的Folding、括号、单词等基础信息,另一个控制了语言的语法tokenization规则。有了这两个文件,就可以获得语法高亮和 基于文本的各类编辑器功能了。

不过,如果想在编辑器内提供一定的自动补全或者代码跳转,还要写一点代码。在VS Code的插件API里包含了Language Server ProtocolAPI。也就是说,通过书写简单的JavaScript代码,可以为VS Code提供语言服务,不理解Language Server Protocol也没关系,因为每个API都可以单独拿出来使用。

下面来实现一个自动补全的插件。可以沿用之前的插件模板,这个插件只有一个代码文件extension.js

不过这一次不是注册一个命令,而是要注册一个自动补全提供者(provider)。先输入vscode.languages.,然后看看自动补全都提示了什么。

languages下能够注册各种不同的语言功能,比如自动补全 (CompletionItemProvider)、代码跳转(DefnitionProvider)、格式化 (DocumentFormattingEditProvider)等等。一个完整的Language Server,其实就将这些方法通通实现,只不过Language Server是运行在另一个独立的进程里。而使用插件API时,选择只实现某些API。这里选择CompletionItemProvider,需要提供三个参数。

第一个是DocumentSelector。控制在哪些文件中提供自动补全。类型可以是字符串,也可以是DocumentFilter。先试用plaintext

第二个是CompletionItemProvider了。这个对象至少要拥有下面这个函数属性。

provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult<CompletionItem[] | CompletionLis>;

VS Code在调用这个函数时,会提供当前的文档document, 光标所在的位置position,用于监测用户取消建议操作的CancellationToken,以及最后一个当前补全项的上下文context

有了documentposition,就能够分析全文代码并且推测当前光标处可能的建议选项了。返回值可以是CompletionItem[]或者CompletionLis

export class CompletionItem {
    label: sring;
    kind?: CompletionItemKind;
    detail?: sring;
    documentation?: sring | MarkdownString;
    sortText?: sring;
    flterText?: sring;
    preselect?: boolean;
    insertText?: sring | SnippetString;
    range?: Range;
    commitCharacters?: sring[];
    keepWhitespace?: boolean;
    additionalTextEdits?: TextEdit[];
    command?: Command;
    consructor(label: sring, kind?: CompletionItemKind);
}

export class CompletionLis {
    isIncomplete?: boolean;
    items: CompletionItem[];
    consructor(items?: CompletionItem[], isIncomplete?: boolean);
}

虽然CompletionItem的属性非常多,但实际上除了label以外,其他都是optional的。label就是建议项的名字,用于建议列表里的显示。不过,除了label,还必须告诉VS Code,当用户从建议列表里选择了这个建议项之后,按下回车,该如何修改光标处的代码。这时候就要使用insertTextrange这两个属性了,它们决定了将哪段代码替换成什么新的文本。

regiserCompletionItemProvider的第三个属性,是triggerCharacters决定了当用户按下哪个字符之后,编辑器就应该立刻询问CompletionItemProvider以提供自动补全建议。比如只在用户按下.(时触发自动补全,那么这里要输入的就是[‘.’, ‘(’]

extension.js

cons vscode = require('vscode');
function activate(context) { 
    vscode.languages.regiserCompletionItemProvider('plaintext', {
        provideCompletionItems: (document, position) => {
            return [
                {
                    label: 'mySuggesion', 
                    insertText: 'mySuggesion'
                } 
            ]
        }
    }, ['.'])
}

exports.activate = activate;

function deactivate() {

}
exports.deactivate = deactivate;

需要将package.json里的activationEvents改成*

"activationEvents": [ "*"],

F5启动插件运行后,打开一个纯文本,输入this.,当输入.之后,立刻看到了建议列表。然后按下回车,插件提供的建议就被插入到编辑器里了。

使用上面的样例代码,当将光标移动到vscode.languages上时,按下F12VS Code就立刻跳转到VS Code插件APItypings文件里。这个文件是所有插件API的定义。如果是在registerCompletionItemProvider上按F12,则是直接跳转到这个API的定义处。可以通过跳转定义命令(F12),一个个查询每个类型。

VS Code的插件API太多了,有了typings文件,不离开VS Code,就能够了解到它们各自是怎么使用的。

8. Decorations 装饰器

首先,使用JavaScript插件模板。extension.js文件内容如下。

cons vscode = require('vscode');
function activate(context) { 
    vscode.commands.regiserCommand('extension.sayHello', () => {
        let decorationType = vscode.window.createTextEditorDecorationType({ backgroundColor: '#fff' });
        let editor = vscode.window.activeTextEditor; editor.setDecorations(decorationType, [new vscode.Range(0, 0, 0, 1)]);
    }); 
}

exports.activate = activate;

function deactivate() {

}

exports.deactivate = deactivate;

注册一个名为extension.sayHello的命令,创建DecorationType

let decorationType = vscode.window.createTextEditorDecorationType({ backgroundColor: '#fff'})

使用vscode.window.createTextEditorDecorationType传入参数对象,对象里添加了属性backgroundColor用于定义背景色的。

获取当前的编辑器对象editor。使用editor上的setDecorations方法,并且传入两个参数,第一个是我创建的DecorationType,第二个是代码范围Range。通过这个API,在代码片段Range上使用decorationType所代表的装饰器,也就是将背景色调整成#fff

运行插件,无论是PigmentRainbow Brackets,还是GitLens,它们修改编辑器内代码颜色和背景色的逻辑跟上面的这一小段代码基本一致。只不过VS CodeDecoration API,有非常多不同的属性可以设置,这也让Decoration效果千差万别。

如果在createTextEditorDecorationType运行F12,就能够跳转到createTextEditorDecorationType函数的定义处,函数的参数类型是DecorationRenderOptionscreateTextEditorDecorationType的结构如下。

export interface DecorationRenderOptions extends ThemableDecorationRenderOptions { 
    isWholeLine?: boolean;
    rangeBehavior?: DecorationRangeBehavior; 
    overviewRulerLane?: OverviewRulerLane; 
    light?: ThemableDecorationRenderOptions; 
    dark?: ThemableDecorationRenderOptions;
}

createTextEditorDecorationType继承自ThemableDecorationRenderOptions,不过它多加了几个属性。比如是否将decoration运用在整行代码上,是否要将颜色渲染在滚动条上等。不过比较重要的两个属性其实是lightdark

lightdark的类型,都是ThemableDecorationRenderOptions,只要设置了这两个值,VS Code就会根据当前的主题是深色还是浅色,决定是加载light还是dark的值,如果这两个值没有设置的话,那么就会查看DecorationRenderOptions上的其他属性。ThemableDecorationRenderOptions有如下属性。

export interface ThemableDecorationRenderOptions { 
    backgroundColor?: sring | ThemeColor; outline?: sring;
    outlineColor?: sring | ThemeColor; outlineStyle?: sring;
    outlineWidth?: sring;
    border?: sring;
    borderColor?: sring | ThemeColor; borderRadius?: sring; borderSpacing?: sring; borderStyle?: sring; borderWidth?: sring;
    fontStyle?: sring;
    fontWeight?: sring;
    textDecoration?: sring;
    cursor?: sring;
    color?: sring | ThemeColor;
    opacity?: sring;
    letterSpacing?: sring;
    gutterIconPath?: sring | Uri;
    gutterIconSize?: sring;
    overviewRulerColor?: sring | ThemeColor;
    before?: ThemableDecorationAttachmentRenderOptions; after?: ThemableDecorationAttachmentRenderOptions;
}

可以给这些属性归归类。

1. Color

代码颜色和背景相关的属性。

    backgroundColor?: sring | ThemeColor; 
    color?: sring | ThemeColor; 
    overviewRulerColor?: sring | ThemeColor;

除了使用类似于#fff这样的字符串以外,还可以使用ThemeColor,也就是颜色主题(themes) 里的颜色定义,比如。

new vscode.ThemeColor('editorWarning.foreground')

这样一来,当切换主题的时候,颜色会随之改变。

2. Border

第二类是Border边框。

border?: sring;
borderColor?: sring | ThemeColor; 
borderRadius?: sring; 
borderSpacing?: sring; 
borderStyle?: sring; 
borderWidth?: sring;

比如对上面的样例代码略作修改。

vscode.commands.regiserCommand('extension.sayHello', () => {
    let decorationType = vscode.window.createTextEditorDecorationType({
        border: '1px solid red;' 
    });
    let editor = vscode.window.activeTextEditor;
    editor.setDecorations(decorationType, [new vscode.Range(0, 0, 0, 1)]); 
});

然后运行代码,就能够给代码块添加边框了。

3. Outline

CSSOutline(轮廓)是用于在Border边框的周围画一条线,以突出元素。我们同样可以使用下面这些配置,来分别控制Outline的各个属性。

    outline?: sring;
    outlineColor?: sring | ThemeColor; 
    outlineStyle?: sring; 
    outlineWidth?: sring;

比如,我们将样例代码修改成如下:

vscode.commands.regiserCommand('extension.sayHello', () => {
    let decorationType = vscode.window.createTextEditorDecorationType({
        outline: '#00FF00 dotted' 
    });
    let editor = vscode.window.activeTextEditor; editor.setDecorations(decorationType, [new vscode.Range(1, 1, 1, 4)]);
})

为了更好地看到效果,这里把range修改成了new vscode.Range(1, 1, 1, 4)

4. Font

可以通过fontStylefontWeight来控制字体,也可以通过opacity来控制透明度,或者使用letterSpacing控制文字之间的间距。

    fontStyle?: sring; 
    fontWeight?: sring; 
    opacity?: sring; 
    letterSpacing?: sring;

比如代码修改为。

vscode.commands.regiserCommand('extension.sayHello', () => {
    let decorationType = vscode.window.createTextEditorDecorationType({ 
        fontStyle: 'italic',
        letterSpacing: '3px'
    });
    let editor = vscode.window.activeTextEditor; editor.setDecorations(decorationType, [new vscode.Range(1, 1, 1, 4)]);
});

5. 其他

除此之外,还可以通过gutterIconPathgutterIconSize在行号旁边添加图标。

    gutterIconPath?: sring | Uri; 
    gutterIconSize?: sring;

比如说VS Code中的断点,就可以通过这个属性在插件中实现。另外,也可以通过beforeafter在某个代码的前面或者后面创建decorations

before?: ThemableDecorationAttachmentRenderOptions; 
after?: ThemableDecorationAttachmentRenderOptions;

CSS文件里颜色前的Color Decorator小方格,就是使用before属性来实现。

DecorationRenderOptions里的大部分属性,其实就是CSS里的各种属性,如果知道如何使用CSS来对元素布局的话,那么使用这些API就难不倒你。不过最难的还是想象力,如何活用这套API,将重要的信息呈现给用户,而又不会打扰到用户的正常体验,这就体现功力了。

比如之前介绍过Import Cost插件,这个插件就巧妙地将每个javascript模块的大小,渲染在了这一行代码的最后,十分显眼,但又不会影响到查看代码。

GitLens插件也有类似的设计,它可以把当前这行是谁写的从Git中读取出来,然后渲染在本行代码的最后,同时这些信息的颜色比较浅,从而不会喧宾夺主,当需要的时候也可以轻松迅速地查看代码的修改信息。文章最开始介绍的Rainbow Brackets等也都是对Decoration API的活用。

9. 工作台 API

第一类是通过插件APIVS Code中调出对话框向用户询问问题,或者弹出信息提示以警示用户。二者的本质是一致的,都是跟用户进行信息的交互,以完成进一步的操作。

首先来看信息提示,这个API在之前的例子里已经使用过了。

比如下面的这段示例代码,注册了extension.sayHello命令,通过vscode.window.showInformationMessage输出提示Hello World

vscode.commands.regiserCommand('extension.sayHello', () => { 
    vscode.window.showInformationMessage('Hello World');
});

至于对话框可以使用的API有两种showWarningMessageshowErrorMessage。使用方法都是一样的,不过呈现效果会不同,用以体现Information、WarningError不同的重要程度。

除了给用户展示信息以外,这三个API还允许和用户进行简单的交互。先来看showInformationMessage的定义。

/**
* Show an information message to users. Optionally provide an array of items which will be presented as
* clickable buttons. *
* @param message The message to show.
* @param items A set of items that will be rendered as actions in the message.
* @return A thenable that resolves to the selected item or `undefned` when being dismissed. 
*/

export function showInformationMessage(message: sring, ...items: sring[]): Thenable<sring | undefned>;

除了传入消息message,还可以传入一个数组,这个数组里的字符串,都会被渲染成按钮,当用户按下这些按钮,就能够收到反馈了。

vscode.commands.regiserCommand('extension.sayHello', () => { 
    vscode.window.showInformationMessage('Hello World', 'Yes', 'No').then(value => {
        vscode.window.showInformationMessage('User press ' + value); 
    })
});

给用户提供了YesNo两个选项,当用户选择其中之一后,弹出一个新的信息框,显示用户点击了哪个。

QuickPick通过vscode.window.showQuickPick函数,给用户提供一系列选项,根据用户选择的选项进行下一步操作。这里给用户提供了三个选项firstsecondthird,然后将用户的选择以信息的方式弹出。

vscode.window.showQuickPick(['first', 'second', 'third']).then(value => { 
    vscode.window.showInformationMessage('User choose ' + value);
})

showQuickPick的第一个参数除了可以是一个字符串数组以外,还可以提供其他不同的类型比如Promise,这个参数可以为最终解析值为字符串数组的Promise。有了这个类型,就能够异步地获取选项列表,等这个列表解析出来了再提供给用户,而用户则会在界面上看到滚动条。

另外,数组也可以是QuickPickItem对象数组。

export interface QuickPickItem {
/**
* A human readable sring which is rendered prominent.
*/
    label: sring; 
    description?: sring; 
    detail?: sring; 
    picked?: boolean;
    alwaysShow?: boolean;
}

上面的示例里面使用的数组也可以用QuickPickItem来替代,只需要使用QuickPickItemlabel属性,然后label里的值就会被渲染在列表中。

除了label还可以通过description或者detail来提供更多的信息。比如说使用下面的QuickPickItem数组。

vscode.commands.regiserCommand('extension.sayHello', () => { 
    vscode.window.showQuickPick([{
        label: 'frs',
        description: 'frs item',
        detail: 'frs item details'
    },{
        label: 'second', 
        description: 'second item', 
        detail: 'second item details'
    }]).then(value => {
        vscode.window.showInformationMessage('User choose ' + value.label);
    }) 
});

至于picked属性就非常好理解了。默认情况下列表里的第一个选项会被选中。如果希望默认选中其他项的话,将它的picked属性改为true就好了。alwaysShow这个属性则是使用于列表很长的情况,如果列表非常长,VS Code不得不渲染出滚动条时,通过将某些项的alwaysShow属性改为true,这个选项就会一直出现在列表中,而不会受滚动条的影响。

整体来讲实现插件的过程中,很多命令或者操作的流程、信息,并不是完全确定的,往往需要用户来提供更多的信息,并且由用户来做出最终的决定。这个时候通过信息提示和QuickPick将选择权交还给了用户。

但是一定要注意的是信息提示和QuickPick都是会打扰用户的正常工作的。所以在使用这类API的时候一定要慎重,不然用户可能就会卸载我们的插件了。

10. 面板 Panel

第二类就是面板里的信息了。默认情况下,面板中有以下4个组件:问题面板,调试面板,输出面板,终端面板。这里面除了调试面板是由调试插件控制的以外,其他的三个,都是可以通过普通的插件API来完成的。这里面属问题面板和输出面板使用最为频繁。

1. 问题面板

在书写代码时VS Code的各类插件会把代码中出现的错误信息提供给问题面板。然后用户就可以通过问题面板,快速地查询问题并且进行代码的跳转。问题面板相关的API存在于vscode.languagesnamespace下。要给问题面板提供相关的信息,使用的APIcreateDiagnosticCollection

export namespace languages { 
/**
* Create a diagnosics collection.
*
* @param name The name of the collection. * @return A new diagnosic collection.
*/
export function createDiagnosicCollection(name?: sring): DiagnosicCollection; }

通过vscode.languages.createDiagnosticCollection创建出来的对象,将是跟VS Code问题面板通讯的中介。下面使用如下的代码样例进行说明。

vscode.commands.regiserCommand('extension.sayHello', () => {
    let collection = vscode.languages.createDiagnosicCollection('myextension');
    let uri = vscode.window.activeTextEditor.document.uri;
    collection.set(uri, [
        {
            range: new vscode.Range(0, 0, 0, 1), message: 'We found an error'
        } 
    ]);
});

collection对象创建出来后,就要往这个collection里塞数据,这里使用的APIset(uri: Uri, diagnosics: Diagnosic[] | undefned): void;

set函数提供两个参数第一个是文档的地址Uri,样例代码里使用了vscode.window.activeTextEditor.document.uri,也就是当前编辑器里的文档的地址Uri。第二个是在这个文档里发现的所有问题,每个问题的类型必须是Diagnostic

export class Diagnosic { 
/**
* The range to which this diagnosic applies. 
*/
    range: Range; /**
    * The human-readable message.
    */
    message: sring; /**
    * The severity, default is error.
    */
    severity: DiagnosicSeverity;

    source?: sring;
    code?: sring | number;
    relatedInformation?: DiagnosicRelatedInformation[];
    tags?: DiagnosicTag[];
    consructor(range: Range, message: sring, severity?: DiagnosicSeverity);
}

Diagnostic对象必须要提供的两个属性是rangemessage,也就是问题所在的位置和问题相关的信息。还可以给Diagnostic对象提供诸如severity问题的程度、source问题的来源等。

代码运行起来后在编辑器里执行Hello World命令,可以看到第一行第一列代码下出现了波浪线,同时问题面板里也多出了一个条目,点击它就能够跳转到编辑器中。

2. 输出面板

输出面板提供内容的API要更简单一些。首先要创建一个OutputChannel。只要提供一个名字即可。接着就可以往这个对象中添加输出日志了。有了这两行代码,就可以运行了。

let channel = vscode.window.createOutputChannel('MyExtension');

channel.appendLine('Hello World');

通过上面的代码可以发现,输出面板下拉框中现在出现了一个新的选项,叫做MyExtension也就是创建的OutputChannel。接着使用channel.appendLine输出的信息,就会被放在输出面板中。这套API非常像console.log(),唯一不同的是这套API将内容输出到了输出面板中。

这部分总体来说就是问题面板的使用,跟语言服务结合到一起会很好,比如Linting信息、编译错误信息,甚至错别字检查信息,都可以塞到问题面板中。不过要注意,问题面板里的内容,意味着需要用户去修改代码。所以一些无关紧要的信息就不要放到这里面了。

而输出面板,完全可以把它当log日志来使用。大部分时间用户不需要去关心它,不过当用户遇到问题了,如果能够通过输出日志里的信息获得帮助,那么输出面板的目的就达到了。

3. 视图 TreeView

这一套API的最初需求是来自于Visual Studio用户,Visual Studio中,可以在视图中看到项目、测试、云管理等,但是VS Code当时并没有API可以实现这种定制。于是TreeView应运而生,通过实现这套API,任何插件都可以实现类似于资源管理器的树形结构。

TreeView虽然是用于创建视图中树形结构的,但是它跟VS Code的其他 API 非常类似,都是给VS Code提供数据,然后VS Code来进行渲染。创建TreeViewAPI也非常简单。

export namespace window {
    export function regiserTreeDataProvider<T>(viewId: sring, treeDataProvider: TreeDataProvider<T>): Disposable;
}

registerTreeDataProvider一共有两个参数第一个是TreeView的名字,第二个是TreeView的数据来源Data Provider

export interface TreeDataProvider<T> { 
    onDidChangeTreeData?: Event<T | undefned | null>; 
    getTreeItem(element: T): TreeItem | Thenable<TreeItem>; 
    getChildren(element?: T): ProviderResult<T[]>; 
    getParent?(element: T): ProviderResult<T>;
}

Data Provider上只有两个属性是必须的。第一个是getTreeItem,通过这个函数,VS Code就知道该怎么渲染某个树节点了。第二个是getChildren,返回一个树节点的所有子节点的数据。TreeItem是每个树节点的数据结构。

export class TreeItem {
    label?: sring;
    id?: sring;
    iconPath?: sring | Uri | { light: sring | Uri; dark: sring | Uri } | ThemeIcon; 
    resourceUri?: Uri;
    tooltip?: sring | undefned;
    /**
    * The command that should be executed when the tree item is selected.
    */
    command?: Command;
    /**
    * TreeItemCollapsibleState of the tree item. 
    */
    collapsibleState?: TreeItemCollapsibleState;
    contextValue?: sring;
    consructor(label: sring, collapsibleState?: TreeItemCollapsibleState); 
    consructor(resourceUri: Uri, collapsibleState?: TreeItemCollapsibleState);
}

TreeItem有两种创建方式第一种是提供label,也就是一个字符串,VS Code会把这个字符串渲染在树形结构中,第二种是提供resourceUri也就是一个资源地址,VS Code则会像资源管理器里渲染文件和文件夹一样渲染这个节点。

iconPath属性是用于控制树节点前的图标的。如果自己通过TreeView来实现一个资源管理器,就可以使用iconPath来为不同的文件类型指定不同的图标。tooltip属性是当把鼠标移动到某个节点上等待片刻,VS Code就会显示出这个节点对应的tooltip文字。collapsibleState用于控制这个树节点是应该展开还是折叠。当然,如果这个节点没有子节点的话,这个属性就用不着了。command属性如果存在的话,当点击这个树节点时,这个属性所指定的命令就会被执行了。

了解了以上几个属性就能够实现一个简易的TreeView了。

vscode.window.regiserTreeDataProvider('myextension', { 
    getChildren: (element) => {
        if (element) { 
            return null;
        }
        return ['first', 'second', 'third']; 
    },
    getTreeItem: (element) => { 
        return {
            label: element,
            tooltip: 'my ' + element + ' item' 
        }
    } 
})

上面的这段代码注册了一个名为myextensionTreeView,这个TreeView只有一层节点,分别是firstsecondthird。将这段代码放入extension.js中时,运行插件会发现VS Code的视图里找不到这个名为myextensionTreeView

cons vscode = require('vscode');

function activate(context) { 
    vscode.window.regiserTreeDataProvider('myextension', {
        getChildren: (element) => { 
            if (element) {
                return null; 
            }
            return ['first', 'second', 'third']; 
        },
        getTreeItem: (element) => { 
            return {
                label: element,
                tooltip: 'my ' + element + ' item' 
            }
        } 
    })
}

exports.activate = activate;

function deactivate() {
}
exports.deactivate = deactivate;

这是因为要想将这个 TreeView 成功地注册到VS Code中,需要在package.jsoncontributes部分添加TreeView的申明。修改后的package.jsoncontributes部分如下。

{
    "contributes": {
        "views": {
            "explorer": [
                {
                    "id": "myextension",
                    "name": "My Extension"
                }
            ]
        }
    }
}

这段contributes是说,把myextension这个TreeView注册到资源管理器中。除了将TreeView注册到资源管理器Explorer下以外,也可以将它注册到版本管理视图中,对应的contributes如下。

"contributes": {
    "views": {
        "scm": [
            {
                "id": "myextension",
                "name": "My Extension"
            }
        ]
    }
}

代码运行起来后,就能够在版本管理视图中看到这个TreeView了。

简言之,VS CodeTreeView使用了Data Provider的模式,插件提供数据,而VS Code负责渲染。至于数据长什么样、树形结构里的层级关系如何,这个就属于Business Logic了,需要开发者自己发挥想象力。比如说GitHub Pull Request插件,用树形结构来展示所有的Pull Requests和每个PR里的代码改动,NPM Explorer则将所有的NPM脚本展示在树形结构中。

除此之外VS Code还有很多别的有趣的工作台相关的API,比如可以使用WebView来生成任意的编辑器内容,可以使用FileSystemProvider或者TextDocumentContentProvider来为VS Code提供类似于本地文件的文本内容。虽然它们很小众也更高级,但是使用的方法,跟上面提到的几种并没有什么区别,建议你通过VS Codetypings文件找寻你想要使用的API多多尝试。

11. 维护和发布

VS Code的插件API的发布流程首先是发出提议Proposal,看看社区的反馈如何。这一类API会出现在vscode.proposed.d.ts文件中,而稳定版本的API则是在vscode.d.ts里。一个API进入proposed状态并不需要什么流程,但是要进入stable的话,就要经过整个团队的review了。基本上一个API要发布到stable中,需满足以下几个条件。

首先,插件API不会将UI直接暴露给插件。VS Code的界面 (也就是DOM)的渲染完全由VS Code控制,插件API可以做的,就是将UI上的渲染逻辑翻译成Data Provider的形式,插件提供内容,VS Code负责渲染。

其次,如果一个API的运行时间可能比较长,那么这个API应该支持Promise,并且可以取消(也就是vscode.d.ts里常看到的Cancellation Token)。

最后,插件API能够正确地处理对象的生命周期。VS Code使用了Dispose模式,大部分VS Code插件API生成的对象,都会拥有一个dispose函数属性,然后运行这个函数就可以将这个对象销毁。

基于上面的这些API设计原则,也能够得出一个好的插件实现应该有如下特性。

对于长时间运行的任务,如果用户选择取消,那么插件应该能够终止任务。 插件能够及时地删除不再使用的对象,以及正确的时候dispose VS Code生成的对象,减少内存的使用。插件在给VS Code插件API提供数据的同时,能够做到增量更新,尽可能地减少VS Code重新渲染组件。只有做到上面这些,才能尽可能地保证插件的性能。插件提供的功能是一方面,但是如果性能出众的话,就真的是一个好插件了。

1. Node.js 模块使用

VS Code插件其实就是一个Node.js应用。那么如何管理Node.jsdependencies也是插件应该关心的。在使用第三方的Node.js模块时,要注意以下几点。

第一,很多简单的功能,其实可以自己实现,过多地使用第三方模块,会导致代码量不必要地增大。代码量增大,就相应地减慢了插件的下载和更新。同时插件被激活时,需要加载各个Node.js模块,模块越多,速度也就越慢。所以使用模块要克制。第二,如果可以的话,借助 webpack 对插件进行打包,并且开启treeshaking,把没有使用的代码删除掉。第三,对于性能要求比较高的应用,可以考虑使用Node.jsNative Module或者Web Assembly。最新版本的VS Code里已经支持了Node.js新的Native Module API (N-API)Web Assembly了。不过这两者之间也各有优劣。

NAPI之前,大家都在使用NAN来管理Node.js Native Module,但是一旦VS Code升级了Electron,导致Node.js版本发生变化,所有的Native Module就不能工作了。NAPI的出现,解决了这个问题,再也不用担心Electron升级的问题了。但是NAPI也并没有解决发布的问题,依然得为每个不同的平台(Windows,macOS,Linux)分别编译Native Module,比较麻烦。

相比于Native Module,使用Web Assembly就要好很多,因为Web Assembly天然就是跨平台的。但是它也有缺点,就是无法访问系统API,如果代码必须要访问到一些原生的API,可能还是得用Native Module

以上的重点依然是性能。对于大部分插件而言business logic都不是特别复杂,而性能往往就是区分度,如果能够借助Native Module、Web Assembly或者Webpack等打包工具,给插件代码提速,就非常给力了。

2. 发布

插件的最终发布跟插件API相比简单很多。只需创建一个Visual Studio Online账户,然后使用vsce这个npm包就能发布了。现在Marketplace更是允许直接在后台发布,而无需使用命令行。关于更多的细节,还请阅读官方文档。

在插件的package.json文件中,有这样一个配置。

"engines": {
    "vscode": "^1.29.0"
}

这段配置的意思是这个插件至少要求用户安装1.29版本的VS Code^1.29.0的书写方式跟npm包的版本书写方式一模一样。

那什么时候需要更新engine值呢?建议是当且仅当使用了某个新的API,而这个 新API要求了用户必须使用某个版本的VS Code时就值得去更新这个engine值了。更新完之后,只有新版本的VS Code用户,才会收到插件的更新,也就是说如果用户还在使用老版本的话,就不会收到更新。

不过也不必担心更新了engine,导致用户量的减少,因为VS Code的大部分用户,都会在新版本发布之后的一到两个月更新到最新的版本,也就是说,很快用户数量就会恢复正常。而且在用户还没有完全升级的情况下,如果有什么bug,还可以及时修复,而不会波及太多的用户。

好了,以上就是插件开发相关的全部内容了。正如一直强调的VS Code的插件开发,跟开发一个Node.js应用没有区别,使用的API都写在vscode.d.ts这个typings文件里。如果想看看这些插件API的样例代码,也可以自行下载试试看。

转载须知

如转载必须标明文章出处文章名称文章作者,格式如下:

转自:【致前端 - zhiqianduan.com】 vscode插件开发  "隐冬"
请输入评论...