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
  • 插件宿主进程
  • 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-hansVS Code 中文(繁體)語言套件: vscode-language-pack-zh-hant 集成到我们下载的 vscode 源码 extensions 中。

locale.json:

{
  // 定义 VS Code 的显示语言。
  // 请参阅 https://go.microsoft.com/fwlink/?LinkId=761051,了解支持的语言列表。

  "locale": "zh-CN" // 更改将在重新启动 VS Code 之后生效。
}

参考

写这些代码也许就一两个小时的事,写一篇大家好接受的文章需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!