vscode 定制开发 —— 基础准备
前言
前端开发现在最流行的 IDE 莫过于 vscode,对于一些特殊需求,例如移动端页面预览、自定义脚手架编译工程等等,如果能够通过 IDE 的插件拓展去完成,那定是一件美美的事情,后续相关工作有这块的需求,所以利用下班时间及周末做一下相关的技术储备,玩一下 vscode 插件拓展及源码改造。
起步
目前 vscode 相关的开发主要有两种方式:插件拓展和"魔改"源码。插件开发相对比较简单,可以从插件市场找到很多一些功能插件然后学习改造。插件拓展在不修改 vscode 源码对基础上对先有 IDE 进行功能增强,但是受限于 vscode 的安全架构模型,功能相对比较局限,特别是 UI 类型的功能拓展改造。
对于一些自定义工程管理的需求,很多时候需要改造界面,这个时候从源码角度入手才方便实现相关的功能,典型的例子:白鹭 Egret Wing,这个堪称对 vscode 的魔改,实现了很多自定义的交互功能。最近发现 weex studio 和 快应用 IDE 也基于 vscode 改造的 IDE,看到这个真的很兴奋,哈哈,很多思路和我之前想的一样,只可惜之前没有去动手实现,所以也催促了对于想干的事情,动手要趁早。
从插件拓展入手
微软官方提供了脚手架 vscode-generator-code 来生成项目结构,官方既然提供了那自然采用官方的靠谱。
安装:
npm install -g yo generator-code
yo code
新建一个测试工程,我 们重点看两个文件:package.json 和 extension.js。
package.json:
"activationEvents": [
"onCommand:extension.helloWorld"
],
"main": "./src/extension.js",
"contributes": {
"commands": [
{
"command": "extension.helloWorld",
"title": "Hello World"
}
]
},
extension.js:
const vscode = require("vscode");
function activate(context) {
console.log('Congratulations, your extension "helloworld" is now active!');
let disposable = vscode.commands.registerCommand(
"extension.helloWorld",
function() {
vscode.window.showInformationMessage("Hello World!");
}
);
context.subscriptions.push(disposable);
}
exports.activate = activate;
vscode.commands.registerCommand 是注册命令的 API,执行后会返回一个 Disposable 对象,所有注册类的 API 执行后都需要将返回结果放到 context.subscriptions 中去。
从官方源码入手
官方文档:https://github.com/Microsoft/vscode/wiki/How-to-Contribute
按照官方文档下载 vscode 源码进行本地编译,由于 vscode 基于 Web 技术栈(HTML+ CSS+JS)构建,所以整体构建方式和前端工程类似。
运行:
yarn watch
./scripts/code.sh
编译成产品包:
yarn run gulp vscode-[platform]
yarn run gulp vscode-[platform]-min
platforms: win32-ia32 | win32-x64 | darwin | linux-ia32 | linux-x64 | linux-arm
以下内容参考源码版本为 v1.32.3.
目录结构:
├── build # gulp编译构建脚本
├── extensions # 内置插件
├── resources # 平台相关静态资源,图标等
├── scripts # 工具脚本,开发/测试
├── out # 编译输出目录
├── src # 源码目录
├── test # 测试套件
├── gulpfile.js # gulp task
└── product.json # App meta 信息
src 下的结构如下:
├── bootstrap-amd.js # 子进程实际入口
├── bootstrap-fork.js #
├── bootstrap-window.js #
├── bootstrap.js # 子进程环境初始化
├── buildfile.js # 构建config
├── cli.js # CLI入口
├── main.js # 主进程入口
├── paths.js # AppDataPath与DefaultUserDataPath
├── typings
│ └── xxx.d.ts # ts类型声明
└── vs
├── base # 通用工具/协议和基础 DOM UI 控件
│ ├── browser # 基础UI组件,DOM操作、交互事件、DnD等
│ ├── common # diff描述,markdown解析器,worker协议,各种工具函数
│ ├── node # Node工具函数
│ ├── parts # IPC协议(Electron、Node),quickopen、tree组件
│ ├── test # base单测用例
│ └── worker # Worker factory 和 main Worker(运行IDE Core:Monaco)
├── code # vscode主窗体相关
| ├── electron-browser # 需要 Electron 渲染器处理API的源代码(可以使用 common, browser, node)
| ├── electron-main # 需要Electron主进程API的源代码(可以使用 common, node)
| ├── node # 需要Electron主进程API的源代码(可以使用 common, node)
| ├── test
| └── code.main.ts
├── editor # 对接 IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持
| ├── browser # 代码编辑器核心
| ├── common # 代码编辑器核心
| ├── contrib # vscode 与独立 IDE共享的代码
| ├── standalone # 独立 IDE 独有的代码
| ├── test
| ├── editor.all.ts
| ├── editor.api.ts
| ├── editor.main.ts
| └── editor.worker.ts
├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏)
├── workbench # 协调editor并给viewlets提供框架,比如目录查看器、状态栏等,全局搜索,集成Git、Debug
├── buildunit.json
├── css.build.js # 用于插件构建的CSS loader
├── css.js # CSS loader
├── loader.js # AMD loader(用于异步加载AMD模块,类似于require.js)
├── nls.build.js # 用于插件构建的 NLS loader
└── nls.js # NLS(National Language Support)多语言loader
其次,由于 VSCode 依赖 Electron,而在上述我们提到了 Electron 存在着主进程和渲染进程,而它们能使用的 API 有所不到,所以 VSCode Core 中每个目录的组织也按照它们能使用的 API 来组织安排。在 Core 下的每个子目录下,按照代码所运行的目标环境分为以下几类:
- common: 只使用 JavaScript API 的源代码,可能运行在任何环境
- browser: 需要使用浏览器提供的 API 的源代码,如 DOM 操作等
- node: 需要使用 Node.js 提供的 API 的源代码
- electron-browser: 需要使用 Electron 渲染进程 API 的源代码
- electron-main: 需要使用 Electron 主进程 API 的源代码
参考:VSCode 源码结构
技术架构
多进程架构
Visual Studio Code 是由微软公司开发的开源、免费、跨平台的代码编辑器,微软希望它在保持核心轻量化文本编辑器的基础上,为编辑器添加项目支持、智能感知和编译调试。
作为微软推出的现代编辑器,它已经在 GitHub 上开源, 使用 Electron 架构,代码编辑器层为 Monaco。
从实现上来看,Electron 的基本结构:
Electron = Node.js + Chromium + Native API
也就是说 Electron 拥有 Node 运行环境,依靠 Chromium 提供基于 Web 技术(HTML、CSS、JS)的界面交互支持,另外还具有一些平台特性,比如桌面通知。
从 API 设计上来看,Electron App 一般都有 1 个 Main Process 和多个 Renderer Process:
- main process:主进程环境下可以访问 Node 及 Native API;
- renderer process:渲染器进程环境下可以访问 Browser API 和 Node API 及一部分 Native API。
VSC 采用多进程架构,VSC 启动后主要有下面的几个进程:
- 主进程
- 渲染进程
- HTML 编写的 UI
- Activitybar: 最左边(也可以设置到右边)的选项卡
- Sidebar: Activitybar 选中的内容
- Panel: 状态栏上方的面板选项卡
- Editor: 编辑器部分
- Statusbar: 下方的状态栏
- Nodejs 异步 IO
- FileService
- ConfigurationService
- HTML 编写的 UI
- 插件宿主进程
- Debug 进程
- Search 进程
主进程
相当于后台服务,后台进程是 VSC 的入口,主要负责多窗体管理(创建/切换)、编辑器生命周期管理,进程间通信(IPC Server),自动更新,工具条菜单栏注册等。
我们启动 VSC 的时候,后台进程会首先启动,读取各种配置信息和历史记录,然后将这些信息和主窗口 UI 的 HTML 主文件路径整合成一个 URL,启动一个浏览器窗口来显示编辑器的 UI。后台进程会一直关注 UI 进程的状态,当所有 UI 进程被关闭的时候,整个编辑器退出。
此外后台进程还会开启一个本地的 Socket,当有新的 VSC 进程启动的时候,会尝试连接这个 Socket,并将启动的参数信息传递给它,由已经存在的 VSC 来执行相关的动作,这样能够保证 VSC 的唯一性,避免出现多开文件夹带来的问题。
渲染进程
编辑器窗口进程负责整个 UI 的展示,也就是我们所见的部分。UI 全部用 HTML 编写没有太多需要介绍的部分。
项目文件的读取和保存由主进程的 NodeJS API 完成,因为全部是异步操作,即便有比较大的文件,也不会对 UI 造成阻塞。IO 跟 UI 在一个进程,并采用异步操作,在保证 IO 性能的基础上也保证了 UI 的响应速度。
插件进程
每一个 UI 窗口会启动一个 NodeJS 子进程作为插件的宿主进程。所有的插件会共同运行在这个进程中。
Debug 进程
Debugger 插件跟普通的插件有一点区别,它不运行在插件进程中,而是在每次 debug 的时候由 UI 单独新开一个进程。
搜索进程
搜索是一个十分耗时的任务,VSC 也使用的单独的进程来实现这个功能,保证主窗口的效率。将耗时的任务分到多个进程中,有效的保证了主进程的响应速度。
定制开发
考虑基于 vscode 源码的定制,我们基本都是因为官方编辑器窗口功能不满足我们的功能,我们需要做一些个性化的功能,下面我们尝试从修改 vscode 原有界面逐步去理解 vscode 的技术架构。vscode 界面主要有以下几个部分,我将结合实际的例子进行研究。
“工作台”是指整个 VS Code UI 和其中包含的下列 UI 组件:
- 标题栏: Title Bar
- 活动栏: Activity Bar
- 侧边栏: Side Bar
- 面板: Panal
- 编辑器群: Editor
- 状态栏: Status Bar
VS Code 提供了各式各样的 API 让在工作台你添加自己的组件。
拓展 ActivityBar
将常用的功能放置于 ActivityBar,点击 ActivityBar 中的图标,然后右边编辑器区域进行切换,这是 vscode 插件中常用的方法,例如:vscode-browser-preview。
views & viewsContainers
ActivityBar 通过 views & viewsContainers 实现相关的功能:
"contributes": {
...
"viewsContainers": {
"activitybar": [
{
"id": "aemp-preview",
"title": "预览",
"icon": "resources/preview.svg"
}
]
},
"views": {
"aemp-preview": [
{
"id": "targetTree1",
"name": "Targets1"
},
{
"id": "targetTree2",
"name": "Targets2"
}
]
},
...
},
contributes.views 提供 VS Code 视图,必须为视图指定标识符和名称。当用户打开视图时,VS Code 将在 onView:${viewId}
上发出activationEvent
(例如 onView:targetTree1,如下例所示)。您还可以通过提供 when context 值来控制视图的可见性。扩展编写者应该通过createTreeView
API 提供数据提供者来创建 TreeView,或者通过 registerTreeDataProvider
API 直接注册数据提供者来填充数据。参考:tree-view-sample。
效果如下:
图标规格:
- Size:图标应为 28x28 并居中
- Color:图标应使用单一单色
- Format:虽然接受任何图像文件类型,但建议图标在 SVG 中。
- States:所有图标都继承以下状态样式:
State | Opacity |
---|---|
Default | 60% |
Hover | 100% |
Active | 100% |
参考文档:contributes.viewsContainers
激活事件-onView:${viewId}
package.json:
"activationEvents": [
"onView:targetTree1",
"onCommand:aemp-debugger.openPreview"
],
extension.ts:
vscode.window.registerTreeDataProvider("targetTree1", new TargetTreeProvider());
context.subscriptions.push(
vscode.commands.registerCommand("aemp-debugger.openPreview", (url?) => {
vscode.window.showInformationMessage("Hello World!");
})
);
TargetTreeProvider.ts:
import * as vscode from "vscode";
import { Commands } from "./commands";
export default class TargetTreeProvider
implements vscode.TreeDataProvider<object> {
private _onDidChangeTreeData: vscode.EventEmitter<
object | undefined
> = new vscode.EventEmitter<object | undefined>();
readonly onDidChangeTreeData: vscode.Event<object | undefined> = this
._onDidChangeTreeData.event;
constructor() {}
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: object): vscode.TreeItem {
return element;
}
getChildren(element?: object): Thenable<object[]> {
vscode.commands.executeCommand("aemp-debugger.openPreview");
vscode.commands.executeCommand('workbench.view.explorer');
// Make sure collection is not cached.
this._onDidChangeTreeData.fire();
return Promise.reject([]);
}
}
ActivityBar 主题定制
ActivityBar 增加文字:
.activitybar .composite-bar li.action-item[title^="Explorer"],
.activitybar .composite-bar li.action-item[title^="Search"],
.activitybar .composite-bar li.action-item[title^="Source Control"],
.activitybar .composite-bar li.action-item[title^="Debug"],
.activitybar .composite-bar li.action-item[title^="Extensions"],
.activitybar .composite-bar li.action-item[title^="资源管理器"],
.activitybar .composite-bar li.action-item[title^="搜索"],
.activitybar .composite-bar li.action-item[title^="源代码管理"],
.activitybar .composite-bar li.action-item[title^="调试"],
.activitybar .composite-bar li.action-item[title^="扩展"] {
margin-bottom: 15px;
}
.activitybar .composite-bar li.action-item[title^="Explorer"]::after,
.activitybar .composite-bar li.action-item[title^="Search"]::after,
.activitybar .composite-bar li.action-item[title^="Source Control"]::after,
.activitybar .composite-bar li.action-item[title^="Debug"]::after,
.activitybar .composite-bar li.action-item[title^="Extensions"]::after,
.activitybar .composite-bar li.action-item[title^="资源管理器"]::after,
.activitybar .composite-bar li.action-item[title^="搜索"]::after,
.activitybar .composite-bar li.action-item[title^="源代码管理"]::after,
.activitybar .composite-bar li.action-item[title^="调试"]::after,
.activitybar .composite-bar li.action-item[title^="扩展"]::after {
position: absolute;
bottom: -12px;
left: 0;
width: 100%;
height: 20px;
color: #aaa;
z-index: 1;
font-size: 11px;
text-align: center;
}
.activitybar .composite-bar li.action-item[title^="资源管理器"]::after,
.activitybar .composite-bar li.action-item[title^="Explorer"]::after {
content: "代码";
}
.activitybar .composite-bar li.action-item[title^="搜索"]::after,
.activitybar .composite-bar li.action-item[title^="Search"]::after {
content: "搜索";
}
.activitybar .composite-bar li.action-item[title^="源代码管理"]::after,
.activitybar .composite-bar li.action-item[title^="Source Control"]::after {
content: "Git";
}
.activitybar .composite-bar li.action-item[title^="调试"]::after,
.activitybar .composite-bar li.action-item[title^="Debug"]::after {
content: "调试";
}
.activitybar .composite-bar li.action-item[title^="扩展"]::after,
.activitybar .composite-bar li.action-item[title^="Extensions"]::after {
content: "插件";
}
ActivityBar 修改宽度:
- legacyLayout.ts 文件
ACTIVITY_BAR_WIDTH = 50
修改为ACTIVITY_BAR_WIDTH = 70
; - activitybarpart.css 修改如下:
.monaco-workbench .part.activitybar {
width: 70px;
}
.monaco-workbench .activitybar > .content {
width: 70px;
...
}
拓展 StatusBar
状态栏上的按钮是可以响应我们创建的命令的,下面我们在状态栏上创建一个 debug 按钮,用来调试运行构建的程序:
let debugButton = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
4.5
);
debugButton.command = __CommandID__;
debugButton.text = `$(bug)调试`;
debugButton.tooltip = "Debug the given target";
debugButton.show();
注:__CommandID__
为需要响应的命令的 ID。
欢迎页
VS Code 欢迎页的实现有多种实现方法,基于 Extension API 的方式或者修改源码的方式。这里我们采用最彻底的方式修改,我们找到欢迎页的源码:
// src/vs/workbench/contrib/welcome/page/welcomePage.ts
const recentlyOpened = this.windowService.getRecentlyOpened();
const installedExtensions = this.instantiationService.invokeFunction(
getInstalledExtensions
);
const resource = URI.parse(require.toUrl("./vs_code_welcome_page")).with({
scheme: Schemas.walkThrough,
query: JSON.stringify({
moduleId: "vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page"
})
});
this.editorInput = this.instantiationService.createInstance(WalkThroughInput, {
typeId: welcomeInputTypeId,
name: localize("welcome.title", "Welcome"),
resource,
telemetryFrom,
onReady: (container: HTMLElement) =>
this.onReady(container, recentlyOpened, installedExtensions)
});
vs_code_welcome_page.ts 里面定义了欢迎页的视图,我们进行必要的修改就行了。
语言包
在 v1.32.3 这个版本的 vscode 源码中,不包含 i18n 相关的包,所以默认的源码中不包含中文字体包,对于我们需要本地化的产品是不太合适的,我们可以从 github 上下载 vscode i18n 的代码。
vscode Localization Extension: https://github.com/Microsoft/vscode-loc
我们默认将 VS Code 中文(简体)语言包: vscode-language-pack-zh-hans 和 VS Code 中文(繁體)語言套件: vscode-language-pack-zh-hant 集成到我们下载的 vscode 源码 extensions 中。
locale.json:
{
// 定义 VS Code 的显示语言。
// 请参阅 https://go.microsoft.com/fwlink/?LinkId=761051,了解支持的语言列表。
"locale": "zh-CN" // 更改将在重新启动 VS Code 之后生效。
}
参考
- Extension API
- VS Code 插件开发文档-中文版
- vscode-extension-samples
- vscode 源码简析
- Visual Studio Code / Egret Wing 技术架构:基础
写这些代码也许就一两个小时的事,写一篇大家好接受的文章需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!