4

“改造” VS Code 编辑器,一起写个插件吧!

 3 years ago
source link: https://my.oschina.net/HelloGitHub/blog/5023408
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
“改造” VS Code 编辑器,一起写个插件吧!

作者:HelloGitHub-小夏(首发于 HelloGitHub 公众号

作为一个靠代码作为“生计”的开发者,bug 写的好不好,编辑器真的很重要!那么 Visual Studio Code 这个大名你肯定不会陌生。作为一个老厉害的编辑器,它的过人之处简单讲讲来说有这么几点:

  • 首先,它的设计者是一个很有名的工程师:Eric Gamma。20年前,他是《设计模式:可复用面向对象软件的基础》的作者之一,这本书在开发社区的地位被视为面向对象软件开发的指路明灯(瞻望大佬)。

  • 其次,对于写 JavaScript 的人来说,虽然市面上有很多大大小小不同的编辑器,比如 sublime、atom、webstorm 等等,VS Code 与他们区别在于他比 sublime 开源,比 atom 更快,比 webstorm 更轻。

VS Code 全名 Visual Studio Code 是微软开源的一款编辑器,GitHub 上标星 115k(11 万)。它是基于 TypeScript 编写,总计代码数量在 30 万以上,大型知名开源项目。

> 项目地址:https://github.com/microsoft/vscode

我们先来简单看一下它的产品定位吧~可以看到,项目作者对它的定位属于轻量级的编辑器,所以要求它轻量、响应速度快,适用于多种语言等等。VS Code 的编辑能力来自于一款同样来自微软叫做 monaco 的开源 Web 编辑器,同时为了实现跨平台的需求又引入了 Electron:一个使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。

正因为有了清楚的定位和方向,才会有了更加清晰的边界。或许你很疑惑,他是怎么支持多种语言的又做到轻量级的?那我们不得不来看一下它的多进程架构。

  • VS Code 有一个主进程入口,负责一些窗口管理、进程间通信、自动更新等全局任务;
  • 渲染进程,顾名思义负责一个 Web 页面的渲染;
  • 插件宿主进程,每个插件的代码都运行在一个独立且隔离的 Node 环境的宿主进程中,插件无法访问 UI;
  • Debug 进程,用于调试;
  • 语言服务,是一种重要的特殊拓展,可以为许多编程语言提供编辑体验,还可以实现 VS Code 支持的自动补充,错误检查(诊断),跳转到定义以及许多其他的语言功能。

最核心的部分就是它的插件系统,为编辑器的拓展带来了更加个性化的开源定制。只要你能找到强大的 VS Code 插件组合在一起,那你的编辑器一定是一个高级且高效率的工作好帮手。

先来看一下,VS Code 大致有哪些可供我们拓展的能力。

有没有心痒痒地想自己动手搞一个 VS Code 插件?下面就带大家做一个入门级的 VS Code 插件。

二、环境准备

首先你搞个 Node.jsGit

其次「全局(-g)」安装 Yeoman(现代 Web 应用程序脚手架工具)和 VS Code Extension Generator 这两个官方指定的工具脚手架(生成 VS Code 插件项目的工具)。

npm install -g yo generator-code

当你看到下面的信息就说明安装成功了:

三、初始化项目结构

依赖环境搞好了,接下来就要用到 Yeoman 这个工具来帮我们快速创建项目结构啦!可能有很多人对这个脚手架不熟悉,简单的说,Yeoman 是一个通用的脚手架系统,它允许创建任何类型的应用程序。你可以用它快速开始新项目。所以不仅仅是 JavaScript 这个语言,Java,Python,C#等都可以用它来生成项目,只要有对应的生成器就可以。那我们进行下一步,在命令行中输入 yo code

让我们来分析一下这几个选项的意思,其实和字面意思一样,从上到下:

  • 新的插件(Typescript)
  • 新的插件(JavaScript)
  • 新的主题颜色
  • 新的语言支持
  • 新的代码片段
  • 新的键值绑定
  • 新的插件包
  • 新的语言包(本土化)

你可以看到这个工具支持创建多种类型的项目,我们今天先从插件(Extension)入手,所以第一个和第二个的区别就是,你要是会用 TypeScript 就选第一个,也是官方推荐的一个;要是不想写各种麻烦的类型定义和类型校验,就选第二个 JavaScript。 这一次我们选 JavaScript 来做一个简单的入门, 随后你会需要填写一系列初始化的信息如下:

注释如下:

  • What type of extension do you want to create?(创建哪一种类型的扩展?)
  • What's the name of your extension?(扩展的名称?应该全部为小写字母,没有空格)
  • What's the identifier of your extension?(扩展的标示?)
  • What's the description of your extension?(扩展的描述是什么?)
  • Initialize a git repository?(是否初始化 git 仓库?)
  • Which package manager to use? (因为我们装的是 npm,所以选 npm 就行了,如果你有 yarn,你也可以选 yarn)
  • 使用哪一种包管理器(来下载各种 npm 包)

四、搞一个简单的 VS Code 插件

前面的准备的差不多啦!那我们就要开始开「绿皮小火车」啦。

进入刚创建的文件目录 cd test-extension,文件目录:

或许你觉得文件目录嘛,一看就知道了,不就是几个配置信息或者项目说明嘛,但是这里面的配置信息「非常重要」x3,重要到你可能少一个配置或者配置的不对,功能就出不来。所以我们还是稍微花点笔墨聊聊这里的信息。

首先你可以在 package.json 文件里面,看到自己在前一个步骤里面设置的各个值,配置内的各个主要的含义如下,这里有个小点注意一下,如果你的 VS Code 比较旧,且更新不了最新的,你就把下面的 engines 设置的版本低一点,比如我就改成了 ^1.52.0 确保一定能兼容目前的 VS Code 编辑器就可以 :

{
  "name": "test-extension", // 插件的名字
  "displayName": "test-extension", // 在插件市场展示的名字
  "description": "vscode extension sample", // 插件描述
  "version": "0.0.1", // 插件版本
  "engines": { // 最低支持 vscode 的版本
    "vscode": "^1.52.0"
  },
  "categories": [ // 插件的类别,用于在插件市场做区分
    "Other"
  ],
  "activationEvents": [ // 插件激活的事件列表,可以有多个触发机制,所以是数组形式
    "onCommand:test-extension.helloWorld"
  ],
  "main": "./extension.js", // 插件主入口
  "contributes": { // 贡献点,用于拓展插件功能的配置项,这里不会细讲,先用 command 举例
    "commands": [
      {
        "command": "test-extension.helloWorld",
        "title": "Hello World"
      }
    ]
  },
  "scripts": {
    "lint": "eslint .",
    "pretest": "npm run lint",
    "test": "node ./test/runTest.js"
  },
  "devDependencies": {
    "@types/vscode": "^1.55.0",
    "@types/glob": "^7.1.3",
    "@types/mocha": "^8.0.4",
    "@types/node": "^12.11.7",
    "eslint": "^7.19.0",
    "glob": "^7.1.6",
    "mocha": "^8.2.1",
    "typescript": "^4.1.3",
    "vscode-test": "^1.5.0"
  }
}

熟悉了配置之后,我们再来看一下插件的入口文件 extsnsion.js ,里面主要有两个主要的函数,一个是 activate 表示激活插件时的处理,一般是注册命令等一些初始化的操作;另一个是 deactivate ,表示插件失活的时候做的处理,其实听名字你也应该有体感,这就是插件的生命周期里的两个钩子函数嘛。

// 引了 vscode 这个模块,这样你就可以用它里面的很多很多功能啦
const vscode = require('vscode');

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {

  console.log('Congratulations, your extension "test-extension" is now active!');

  let disposable = vscode.commands.registerCommand('test-extension.helloWorld', function () {
    vscode.window.showInformationMessage('Hello World from test-extension!');
  });

  context.subscriptions.push(disposable);
}

function deactivate() {}

module.exports = {
  activate,
  deactivate
}

我们来分析一下上面这段代码,你可以看到里面 registerComman 了一个命令,是不是有种似曾相识的感觉?没错,就是上面在 package.json 里面有提到的那个 command,让我们摘出来一起看看:

...,
 // package.json
 "contributes": { // 贡献点,用于拓展插件功能的配置项,这里不会细讲,先用 command 举例
    "commands": [
      {
        "command": "test-extension.helloWorld",
        "title": "Hello World"
      }
    ]
  },
...
...
// extension.js
function activate(context) {
  console.log('Congratulations, your extension "test-extension" is now active!');

  let disposable = vscode.commands.registerCommand('test-extension.helloWorld', function () {
    vscode.window.showInformationMessage('Hello World from test-extension!');
  });

  context.subscriptions.push(disposable);
}
...

这样看起来是不是很直观了?在 package.json 里面设置的 command 的值,就是 extension.js 里面 registerCommand 的值。那这几行命令是什么意思呢?让我们一起来运行看看:

他会帮你打开一个新的 VS Code 编辑器,插件只会加载到这个编辑器中。现在我们使用调用快捷键(MacOS) command+p ,输入 >Hello World (不区分大小写):

回车一下,你会发现在右下角一个不起眼的角落里输出了这么一个提示:

我相信聪明的你们结合代码一定已经恍然大悟了对不对!不知道你们有没有这个疑问,上面那个 console.log 去哪里看?别急,我们回到插件代码的那个编辑器中,仔细看下面这边,他会在我们输入上面的命令之后才出现,因为在 package.json 里面我们配置插件的激活时机就是 onCommand:test-extension.helloWorld

那我们现在抱着「刻意学习」的思路,改一下上面的代码,比如把 test-extension 改成 test

...,
// package.json
"activationEvents": [
    "onCommand:test.helloWorld"
],
...,
"contributes": {
  "commands": [
    {
      "command": "test.helloWorld",
      "title": "Hello World"
    }
  ]
},
...
// extension.js
...
function activate(context) {

  console.log('我在这里!!');

  let disposable = vscode.commands.registerCommand('test.helloWorld', function () {
    vscode.window.showInformationMessage('我改变了 command 的名字!');
  });

  context.subscriptions.push(disposable);
}
...

再按照上面说的触发方法再来一遍,发现依旧是可以的!所以这里的名字只是一个命名空间,你可以改成你想要的任何名字,来适用于比较复杂的插件体系。既然是个命名空间,那其实不要这个前缀也可以。

五、实现一个属于自己的插件

前面介绍了那么多,大家有没有发现其实这个体系也不难,有大佬在前面铺路,其实我们只要按照规则“填空”就好了,那现在我们就来实现一个小小的功能——加一个按钮和他的点击事件。

修改我们的 package.json 如下,因为当前我希望插件加载的时候就已经订阅了按钮的点击事件,所以这里我们可以把 activationEvents 改成 *,这样的话我们的插件在一开始就可以激活并注册事件了:

...,
"activationEvents": [
        "*",
],
"contributes": {
  "commands": [
    {
      "command": "test.helloWorld",
      "title": "Hello World"
    },
    // 注册一下按钮点击的事件,再配一个小图标
    {
      "command": "test.button",
      "title": "按钮",
      "icon": {
        "light": "./media/light/preview.svg",
        "dark": "./media/dark/preview.svg"
      }
    }
  ],
  // 在这里加一下下面这个配置
  "menus": {
    "editor/title": [
      {
        "command": "test.button",
        "group": "navigation"
      }
    ]
  }
},
...

然后回到我们的 extension.js 里面增加一个简单的消息提醒:

function activate(context) {
  console.log('我在这里!!');
  let disposable = vscode.commands.registerCommand('test.helloWorld', function () {
    vscode.window.showInformationMessage('我改变了 command 的名字!');
  });
  // 新增一个按钮的点击命令操作内容
  let button = vscode.commands.registerCommand('test.button', function () {
    vscode.window.showInformationMessage('按钮点击');
  });
  // 记得这个新的命令也要放进去订阅一下
  context.subscriptions.push(disposable, button);
}

看一下效果:

是不是很简单的就自定义了 VS Code 的样式?那我们现在就来分析一下我们上面做的事情。首先,我们修改了 package.json 里的配置,增加了一个 menus ,这个 menus 是什么呢?答案是菜单。菜单项定义包含选择时应调用的命令以及该项应显示的条件(when),所以你也可以给这个菜单项显示加个显示的逻辑,比如我们规定在打开 javascript 文件时才显示这个按钮:

{
  "contributes": {
    "menus": {
      "editor/title": [
        {
          "when": "resourceLangId == javascript",
          "command": "test.button",
          "group": "navigation"
        }
      ]
    }
  }
}

group 的含义呢,是用来定义菜单项的排序和分组的。来自官网的一个图,表示不同的 group 之间存在的顺序关系,当然这个菜单不是上面我们写的那个,而是 editor/context ,所以不同的菜单之间的 group 其实是有细微差别的,但是大体都差不多,而 navigation 的显示优先级是最高的:

我们也可以加一个这个看看:

"menus": {
  "editor/title": [
    {
      "command": "test.button",
      "group": "navigation",
      "when": "resourceLangId == javascript"
    }
  ],
    "editor/context": [
      {
        "command": "test.button",
        "group": "navigation",
        "when": "resourceLangId == javascript"
      }
    ]
}

效果是一样的:

如果你好奇还有哪些菜单,我这里简单整理「翻译」了一下官网的内容(仅常见菜单非全部):

配置菜单项的名称 菜单位置 commandPalette 全局命令面板 explorer/context 资源管理器上下文菜单 editor/context 编辑器右键上下文菜单 editor/title 编辑器标题栏,不配置图片就显示文字 editor/title/context 编辑器标题右键上下文菜单 debug/callstack/context 调试栈视图的上下文菜单 debug/toolbar 调试工具栏 scm/title SCM 标题菜单 view/title 看标题菜单 touchBar macOS 触摸栏 timeline/title 时间轴视图标题菜单栏 extension/context 扩展程序视图上下文菜单

六、做个总结

从上面的简单例子可以看出,VS Code 不仅可以支持我们自定义想要的命令,也允许我们在「限定范围内」对编辑器进行个性化的拓展。为什么说是限定范围呢?因为按官网的话来说,目前插件体系有下面这些局限性:

插件不具备访问 VS Code UI 的 DOM 的能力。所以不能将自定义的 CSS 应用于 VS Code 或将 HTML 元素添加到 VS Code UI 的扩展中去。这样的限制在于:

  • 确保用户的操作在可控范围内,保证操作的一致性
  • 防止因为底层 Web 技术的更迭导致对一些已存在的插件会有影响
  • 保证开发人员可以继续在原有插件基础上进行一如往常的迭代,而不用再去打破原有的规则重新学习

其实我们今天的内容只是工作区拓展很小一部分内容,要学习这个庞大的体系,还是要不断的努力学习呀!下一次,我们来走进「声明类语言特性」,想知道编辑器里的自动提示和补全是怎么做到的嘛?


关注 HelloGitHub 公众号 收到第一时间的更新。

还有更多开源项目的介绍和宝藏项目等待你的发掘。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK