一.扩展能力
VS Code插件不适合做UI定制,比如Atom的tool-bar 在VS Code很难实现:
提供了丰富的扩展能力模型,但不允许插件直接访问底层UI DOM(也就是说插件难以改变IDE外观,UI定制受限),这是出于方便底层持续优化考虑:
With VS Code, we’re continually trying to optimize use of the underlying web technologies to deliver an always available, highly responsive editor and we will continue to tune our use of the DOM as these technologies and our product evolve.
UI DOM这一层可能会随着优化频繁变动,VS Code不希望这些优化项受限于插件依赖,所以干脆把UI定制能力限制起来
除UI定制之外的,IDE相关的功能型特性都是支持扩展的,如基础的语法高亮/API提示、引用跳转(转到定义)/文件搜索、主题定制,高级的debug协议等等
P.S.实际上,非要扩展UI,也是有办法的(逃出插件运行环境,但要费不少力气),具体见access electron API from vscode extension,后续笔记会详细介绍
二.运行环境
为了性能与兼容性,插件在独立的进程(称为extension host process)中运行,并且不允许直接访问DOM,所以提供了一套内置的UI组件,比如智能提示(IntelliSense)
所以插件崩溃或无响应不影响IDE正常运行,例如:
// ref: my-extension/src/extension.ts
export function activate(context: vscode.ExtensionContext) {
// hang up
while (true);
}
一个插件的死循环并不影响IDE的正常使用和其它插件的加载/激活,但在进程列表能够看到Code Helper的CPU占用接近100%,进程级沙箱保证了插件机制的稳定性
三.核心理念
稳定性:插件隔离
插件可能会影响启动性能和IDE自身的稳定性,所以通过进程隔离来解决这个问题,插件运行在独立的进程中,不影响IDE及其启动时间
这样做是从用户角度考虑的,希望用户对IDE拥有完全的控制力,无论插件在做什么,都不影响IDE基本功能的正常使用
P.S.extension host process是个特殊的Node进程,能够访问VS Code扩展API,VS Code也对这种进程提供了debug支持
性能:插件激活
插件都是懒加载的(as late as possible),只在特定场景才加载/激活,所有在此之前也不耗费内存等资源
实现上是插件注册特定激活事件(activation events),由IDE来触发执行,比如markdown插件只在用户代开md文件时才需要激活
激活方式
插件有6种激活方式:
onLanguage:${language} 打开特定语言的文档
onCommand:${command} 通过Command Palette执行特定命令
onDebug 进入调试模式
workspaceContains:${toplevelfilename} 打开的文件夹里含有特定文件
onView:${viewId} 展开指定view
* 打开IDE就激活
除"activationEvents": ["*"]
外都是条件激活,只在特定场景或满足特定条件时才加载/激活插件
插件清单文件
清单文件用来描述插件的meta信息,直接把package.json
作为清单文件,并增加了一些特有字段,比如触发插件加载的激活事件(activation events
)、插件想要增强的扩展点(contribution points
)
IDE在启动过程中扫一遍插件清单文件,UI相关的就扩展UI,UI无关的就把扩展点与插件功能关联起来
另外,由于插件的执行环境是Node进程,所以npm package都是可用的,依赖模块同样声明在package.json
里。注意,用户安装插件时不会自动npm install
,所以需要在发布插件前把依赖模块打包进去,具体见Installation and Packaging
P.S.扩展点类似于AOP里的Join point(连接点),即“允许在这里扩展/增强”,比如新增一个自定义命令,就是对commands
扩展点的增强
manifest
// package.json
{
// 插件名称
"name": "my-extension",
// 显示名称
"displayName": "MyExtension",
// 描述信息
"description": "An awesome vscode extension",
// 版本号 semver格式
"version": "0.0.1",
// 在插件市场展示的图标
"icon": "img/icon.png",
// 发布者名字
"publisher": "ayqy",
// vscode版本要求
"engines": {
"vscode": "^1.19.0"
},
// 所属分类,可选Languages, Snippets, Linters, Themes等等
"categories": ["Other"],
// 加载/激活方式
"activationEvents": ["onLanguage:javascript"],
// 入口文件路径
"main": "./out/extension",
// 注册扩展点关联
"contributes": {
"languages": [
{
"id": "javascript",
"aliases": ["JavaScript", "javascript"],
"extensions": [".js"]
}
]
}
}
P.S.完整的见Extension Manifest File – package.json
extension.ts/activate
只触发一次,根据package.json
声明的activationEvents
来触发,触发条件可以是打开特定语言的文件,或者执行特定命令。激活之后,直到IDE被关闭/崩溃才会触发extension.ts/deactivate
,所以一般用法是:
activate: 插件被激活,初始化功能模块单例(只执行一次)
deactivate: IDE即将关闭,清理现场,但不宜做太耗时的操作,因为据说最多只等待10s
扩展点
即支持的扩展类型,都声明在package.json/contributes
下,包括:
configuration 插件配置项,用户可以通过Settings设置
configurationDefaults 插件配置项默认值
commands 添加命令,用户可以通过Command Palette输入特定命令激活插件功能
menus 添加与命令关联的菜单项,用户点击菜单项时执行对应命令
keybindings 添加与命令关联的快捷键,用户按下特定快捷键时执行对应命令
languages 与文件类型建立关联或扩展新语言,用户打开(满足某些要求的)特定文件类型时执行对应命令
debuggers 添加debugger,通过VS Code debug协议与IDE通信
breakpoints 配合debuggers,声明对debugger支持的(编程)语言类型
grammars 新增TextMate语法描述,语法高亮
themes 添加定制主题
snippets 添加代码片段
jsonValidation 添加json格式校验
views 新增左侧文件查看器视图和调试视图分栏
problemMatchers 添加错误匹配,从lint结果解析出error,warning等
problemPatterns 配合problemMatchers,定义匹配模式
menus
是唯一的UI扩展官方途径,支持扩展的菜单具体如下:
Command Palette搜索框下方菜单 commandPalette
文件查看器右键菜单 explorer/context
编辑器
右键菜单 editor/context
标题栏菜单 editor/title
标题栏右键菜单 editor/title/context
调试视图
调用栈右键菜单 debug/callstack/context
SCM(源码管理)视图
标题栏菜单 scm/title
文件分组菜单 scm/resourceGroup/context
文件状态菜单 scm/resource/context
文件变动菜单 scm/change/title
左侧视图
文件查看器分栏 view/title
调试视图分栏 view/item/context
P.S.都是些不起眼的位置,大刀阔斧的UI定制是不支持的,比如想在左端侧边栏(Activity Bar)加个Icon都是做不到的
标题栏上的菜单扩展支持自定义icon,但定义方式比较奇怪,例如:
"commands": [{
"command": "markdown.showPreviewToSide",
"title": "%markdown.previewSide.title%",
"category": "Markdown",
"icon": {
"light": "./media/PreviewOnRightPane_16x.svg",
"dark": "./media/PreviewOnRightPane_16x_dark.svg"
}
}],
"menus": {
"editor/title": [
{
"command": "markdown.showPreviewToSide",
"when": "editorLangId == markdown",
"alt": "markdown.showPreview",
"group": "navigation"
}
]
}
给command
定义icon
,menu
关联到command
,然后menu
展示对应的icon
扩展API
环境隔离让严格限制插件可用API变得容易很多,插件只能访问IDE提供的扩展性API,不能胡乱搞事情(比如修改UI DOM和样式,官方支持的主题定制项除外)
API设计原则
插件API遵循一些原则:
基于Promise:异步操作都用Promise来描述
取消token:传入
CancellationToken
作为额外参数来检查取消状态,以及接收取消通知可释放式资源管理:持有的资源都需要手动释放,例如事件监听,命令,UI交互等
事件API:调用订阅方法(
on[Will|Did]VerbNoun
)传入listener(接收event
参数)返回Disposable严格空检查:通过TypeScript严格区分
undefined
和null
P.S.关于“可释放式”(Disposable)的更多信息,请查看Dispose pattern
API概览
API按命名空间组织,全局命名空间如下:
commands 执行/注册命令,IDE自身的和其它插件注册的命令都可以,如executeCommand
debug 调试相关API,比如startDebugging
env IDE相关的环境信息,比如machineId, sessionId
extensions 跨插件API调用,extensionDependency声明插件依赖
languages 编程语言相关API,如createDiagnosticCollection, registerDocumentFormattingEditProvider
scm 源码版本控制API,如createSourceControl
window 编辑器窗体相关API,如onDidChangeTextEditorSelection, createTerminal, showTextDocument
workspace 工作空间级API(打开了文件夹才有工作空间),如findFiles, openTextDocument, saveAll
比如可以通过workspace.findFiles + languages.registerDefinitionProvider
实现Haste的全局模块引用跳转支持
另外,一些API以命令形式提供(即上面提到的“IDE自身的”命令),例如vscode.previewHtml
、vscode.openFolder
、editorScroll
等等
基于协议的扩展
插件进程与IDE之间通过特定协议来通信,实现上是以JSON形式的stdin/stdout来通信
这种模式更强大的一点是:插件可以用任意语言来实现,只要遵守这套约定的通信协议即可
四.语言相关扩展
通过配置文件来支持语法高亮、代码片段和智能括号匹配,更复杂的通过扩展API或language server来做
配置型扩展
语法高亮:基础支持区分字符串、注释、关键字等语法角色,高级支持变量、函数引用等语义区分
代码片段:snippets快捷输入,基础支持简单占位符,高级支持嵌套占位符
智能括号匹配:高级支持自动补充成对出现的东西,比如括号、引号、跨行注释等
注意,语言扩展VS Code支持标准Text Mate Grammar(tmLanguage
格式),比如Monaco Editor的非主流Monarch-style友好很多,具体见Colorization Clarification
编程型扩展
简单配置搞不定的,都通过扩展API(写插件)来实现,有2种方式:
实现language server protocol与IDE通信,完全独立
注册Provider提供自定义能力,类似于hook的方式
使用上,第一种麻烦但更强大灵活,第二种方便直接但没那么灵活。支持的扩展能力如下:
hover提示:基础支持类型、文档等信息,高级支持方法签名语法高亮
补全提示:高级支持在补全提示项旁边展示额外信息
检查报错:基础支持保存时对打开的文件内容检查报错,高级支持对打开的文件目录里的任意资源检查报错
方法签名:基础支持在方法签名中包含参数说明文档
跳转到定义:基础支持存在多处定义时都展示出来
引用查找:基础支持返回所有引用处的具体位置
选中查找高亮:基础支持返回当前文档的所有相同引用
方法/变量声明目录:基础支持返回文档中声明的所有标识符,及其定义位置
快速修复:对Warning和Error给出建议做法,快捷修复。基础支持纠错动作,高级支持修改源码,比如重复代码提出函数
上下文操作选项:允许根据用户处代码上下文,提供额外的信息与可操作选项。基础支持展示,高级可以添加自定义命令
重命名:基础不支持按引用重命名,高级支持工作空间下跨文件重命名
代码格式化:基础不支持代码格式化,高级支持全文/选中/输入中格式化
五.开发步骤
环境要求
步骤
通过脚手架生成项目模版:
yo code
命令交互选择插件类型:
New Extension (TypeScript)
New Extension (JavaScript)
New Color Theme
New Language Support
New Code Snippets
New Extension Pack
建议TypeScript,其它都是字面意思,其中Extension Pack(插件包)比较有意思,即插件组装成的插件,类似于React Native的Nuclide
输入插件名称等meta信息,就得到一个插件项目,然后用VS Code单独打开该项目(工作空间不能有其它项目目录),F5启动debug进入插件调试
插件入口文件是my-extension/src/extension.ts
,项目结构规范可以参照VS Code内置插件:
// ref: https://github.com/Microsoft/vscode/tree/master/extensions/markdown
markdown/
media/
*.svg
*.css
snippets/
markdown.json
syntaxes/
*.tmLanguage
src/
features/
*Provider.ts
typings/
*.d.ts
commandManager.ts
commands.ts
logger.ts
markdownEngine.ts
security.ts
telemetryReporter.ts
六.打包发布
提供了CLI工具,vsce:
npm install -g vsce
打包
进入插件目录,打包成.vsix
文件:
cd my-extension
vsce package
会得到一个my-extesion.vsix
本地包(包括node_modules
依赖),然后不想公开的话,自己想办法传播安装,因为不像npm registry,可以手动部署一份,在内网环境放私有插件,Visual Studio Marketplace(VS Code插件市场)没有这么开放的心态:
If you want to share your extension with others privately, you can send them your packaged extension .vsix file.
(见Sharing Privately with Others)
没有办法部署一套Visual Studio Marketplace,所以只能想办法手动解决插件更新问题,比如自动下载/提示安装
发布
要发布到插件市场的话,需要做几件事情:
进入Security页面创建个Personal Access Token
vsce create-publisher (publisher name)
命令新增publishervsce login (publisher name)
命令登录vsce publish -p <token>
命令发布
感谢分享
感谢分享