前言
前段时间做sketch插件的时候,遇到一个场景,每次开发前都要手动删除/注释掉log。左右一寻思,那么多文件,每次开发前都要挨个文件删log也太费劲了。所以寻思找找插件,不过都不太能满足场景,那就自己整一个吧。
解释下为什么要手动注释log: 因为sketch70 - 72 版本的devTool有性能问题,每条log大概会阻塞进程2-3秒,所以开发的时候最好尽量少的输出log。并且项目是好几个人同时开发,所以只能在启动前把所有log都注释掉,然后再写自己的log。
构建项目
装脚手架
npm install -g yo generator-code
脚手架构建项目(注意: code 不是项目名)
yo code
然后就会蹦出一堆选项(和 vue、react 这些脚手架一样),不想填的就回车就行。然后项目就构建好了。
大致的目录结构:
project
├── .vscode // 一般情况下不需要关注
│ ├── launch.json // 插件加载和调试的配置
│ └── tasks.json // 配置TypeScript编译任务
├── extension.js // 入口文件(可以在 package.json 修改)
├── package.json // 整个插件的配置都在里面
我们需要关注的其实就只有两个文件 “package.json” 和 “src/extension.json” 。
提示:本地运行插件直接按“F5”(mac: fn + 5),或者用vscode侧栏的debug(侧栏第四个图标,三角形上爬了只小虫子)的 run Extension
配置文件 package.json
下面简单介绍一下我们后续会用到的配置,详细的配置在讲到功能时再展开,一些额外的配置这里就不说了。
{
"name": "console-key", // 插件的名字 // 定义命令的时候的前缀
"activationEvents": [ "*" ], // 启动插件的触发条件
"main": "./src/index.js", // 入口文件
"contributes": { // 核心配置
"configuration": {}, // 设置可配置项
"commands": [], // 配置命令
"keybindings": [], // 给自己的命令绑定快捷键
"menus": {}, // 可以给vscode各个功能菜单加咱自己的小按钮
},
}
先说下“activationEvents” 和 “main”,其他属性先混个眼熟,后面再讲。
“activationEvents”: 用来告诉vscode,该什么时候启动你的插件。比如说,如果配置值为["onLanguage:javascript", "onLanguage:json"]
, 这就表示打开js文件或者json文件时才会激活你的插件。而我在前面写的 [ "*" ]
就表示任何时候,也就是vscode启动之后就会自动启动插件。【更多配置-英文】【更多配置-中文】
“main”:表示插件的入口文件的地址,默认值应该是“./extension.js”, 为了符合我自己的编程习惯,我就改成了 “./src/index.js”
入口文件
入口文件内置了两个函数,activate 和 deactivate,在插件激活的时候就会立即执行 activate,所以初始化、注册命令和执行的代码就写着这里面。而deactivate就是在插件关闭的时候执行的,比如在插件被卸载、禁用以及vscode窗口关闭的时候,deactivate就会被调用,通常你可以在这个时候做一些清理工作,比如退出登录、清理缓存、弹say goodBye提示...
解释一下什么叫注册命令:简单的来说,我们需要定义一些命令,然后再告诉vscode什么时候分别执行,执行的时候要怎么执行。
总结起来的流程就是:
- 定义一条命令
- 告诉vscode该什么时候执行这条命令
- 告诉vscode执行这条命令的时候做什么
所以说,我的插件其实是由一条条命令组成的,以命令驱动的形式运行。
小 demo
在正式开始撸代码之前,我们看一个小demo,先过一遍流程。
demo:给鼠标右键的功能列表加个“say Hello”选项,点击就提示“hellow world!”
第一步:定义一条命令
很简单,在 package.json 文件里的 contributes.commands 加添加个子对象就行
{
"contributes": {
"commands": [
{ // 新增一个子对象
"title": "say Hello", // 命令的名字
"command": "test-plugin.helloWorld" // 命令的标识(自定义)
},
],
},
}
如上,我们创建了一个标识为“test-plugin.helloWorld” 的命令“say Hello”
第二步:告诉vscode该什么时候执行这条命令
比如我们想在文件里,点击鼠标右键出现的菜单里加个"say Hello"的选项,一点击就会执行这条命令
如上图,我们也是需要在 package.json 里加两条配置
{
"contributes": {
"commands": [
{
"title": "say Hello",
"command": "test-plugin.helloWorld"
},
],
"menus": { // 新增 menus 属性
"editor/context": [ // 表示文件内点击鼠标右键出现的菜单列表
{ // 菜单里面加一个选项
"when": "editorFocus", // 表示文件聚焦之后,再点击鼠标右键,才会出现这个选项
"command": "test-plugin.helloWorld", // 选项对应的命令标识,要和 commands 里的命令标识保持一致
"group": "navigation@1" // 表示选项出现的位置(如下图)
}
]
}
},
}
选项的位置:
到这,我们前两步就完成了,现在就差最后一步:告诉vscode执行这条命令的时候做什么
第三步:告诉vscode这条命令被触发的时候该做什么
实际上就是给咱们之前定义的命令绑定一个回调函数,命令被触发的时候,执行回掉函数。( 有点像 addEventListener )
// -- index.js (入口文件)
const vscode = require('vscode'); // 内置模块 包含了插件的API
// 介绍入口文件的时候提过,该插件启动时自动执行
export function activate(context) {
// 注册一个命令,该API接收两个参数 [ID, callback]
// @ID 在package.json中注册的命令标识
// @callback 执行该命令的时候需要执行的函数
// 这里相当于告诉vscode,命令“test-plugin.helloWorld”被触发时执行函数“sayHello”
let disposable = vscode.commands.registerCommand('test-plugin.helloWorld', sayHello);
// 将刚才注册的命令塞进 vscode 的订阅列表
context.subscriptions.push(disposable);
}
function sayHello (){
// API “showInformationMessage” 可以自动消息提醒,参数是“提醒的文本”
vscode.window.showInformationMessage('Hello World!');
}
然后“F5”一键运行就完事了(mac fn + 5)
插件 “Console Key”
来了老弟~ 下面就是“前言”中提到过的一个关于处理log信息的插件。然后我会详细讲一下完整的开发流程,以及一些API的运用扩展(这个是重点,一定不要忽略)。
插件功能主体分为两部分:
- 一键插入log
- 一键删除log
一键插入log
功能效果
Position 和 Range
开始前先补充两个很重要的属性,Position(位置)和Range(范围),一会会频繁用到
- Position(位置):字符的位置信息,行号和列号。以下面的代码段为例,字符“a”的处在第一行的第七个字符的位置,所以"a"的position为
line:0, character:6
(行和列都是从0开始) - Range(范围):两个位置(position)之间的内容就是范围,所以一个Range是由“开始”和“结束”两个position组成的。以下面的代码段为例,字符串“const”,开始于第一行的第一个字符(line:0,character:0), 结束于第一行的第六个字符(line:0,character:5)。所以"const"的字符为
start:{line:0, character:0}, end:{line:0, character:5}
const a = 1
let b = 2
逻辑
别看下面实现代码挺多,实际上除去注释的有效代码只有addConsole函数里的二十多行。详细实现逻辑如下:
- 获取当前打开的文件的editor(一系列编辑当前文件的api都在里面)
- 通过editor获取当前文件中被选中的文本的位置信息Ranges(数组)
- 通过位置信息获取对应的文本,并生成log, 比如:文本:“first”, log:“console.log('first:', first)”
- 光标换行(不换行直接插入log,会被吊起来抽小皮鞭的)
- 为了知道往哪里插入log,还需要拿到换行之后新的Ranges,然后转换成position
- 调用editor.edit往对应的位置插入log
代码
// -- index.js (入口文件)
const vscode = require('vscode');
// 插件启动之后自动执行
function activate(context) {
// 注册 “一键插入log” 的命令
let disposable = vscode.commands.registerCommand('test-plugin.addConsole', addConsole);
context.subscriptions.push(disposable);
}
function addConsole () {
// 获取当前打开的文件的editor
const editor = vscode.window.activeTextEditor
// editor === undefind 表示没有打开的文件
if(!editor) return;
const textArray = []
// 当前被选中文本的位置信息数组(实际上就是range组成的数组)
const Ranges = editor.selections
Ranges.forEach((range) => {
// 通过位置信息拿到被选中的文本,然后拼接要插入的log
const text = editor.document.getText(range)
let insertText = 'console.log();'
if (text) {
insertText = `console.log('${text.replace(/'/g,'"')} : ', ${text});`
}
textArray.push(insertText)
})
// “光标换行” 调用vscode内置的换行命令,所有focus的光标都会换行
vscode.commands.executeCommand('editor.action.insertLineAfter')
.then(()=> {
const editor = vscode.window.activeTextEditor;
const Ranges = editor.selections;
const positionList = []
Ranges.forEach((range, index) => {
// 通过range拿到start位置的position
const position = new vscode.Position(range.start.line, range.start.character);
positionList.push(position)
})
// 编辑当前文件
editor.edit((editBuilder) => {
positionList.forEach((position, index) => {
// 通过”坐标点“插入我们之前预设好的log文本
editBuilder.insert(position, textArray[index]);
})
});
})
}
module.exports = {
activate
}
自动选择文本
前面输出log的时候,需要先双击选择目标变量,所以为了更省事一点(吃饱了撑的),我们要加个自动选择文本的功能,就像这样子:
先选中光标左边的第一个单词,然后再输出log。
“选中光标左边的第一个单词”,这个操作是不是很熟悉?没错,这就是懒人必备的“shift+alt+left”快捷键的功能。也就是说,vscode本身是有这个功能的,所以咱们能不能直接拿来用呢?当然是可以的,这就是下面要说的,vscode内置功能的调用。
代码
async function addConsole () {
// 只需要加这一行,在开启其他逻辑之前先调用内置命令"cursorWordLeftSelect"
// 需要注意的是,所有命令返回的都是promise,所以需要加个 async await 等待一下
await vscode.commands.executeCommand('cursorWordLeftSelect')
...... 其他代码
}
内置命令的类型
还记得前面实现“光标换行”的功能吗,调用的也是vscode的内置命令vscode.commands.executeCommand('editor.action.insertLineAfter')
,'editor.action.insertLineAfter'就是“光标换行”命令的标识,而这个命令是可以在官方文档上查到的【文档戳这里】
但是,“选中光标左边的第一个单词”的命令“cursorWordLeftSelect”在官方文档里是找不到的,官方也不推荐使用(只是不推荐,又不是不能用😂)
所以我把vscode的内置命令大致分成下面3种:
1、内置命令,可以在官网查到命令标识(ID)
2、内置命令,在官网查不到,但是可以在vscode软件的“键盘快捷方式”中找到。
3、我们自己定义的命令或其他插件定义的扩展命令。
下图画红线的部分,就是其他插件的扩展命令标识(ID)
没想到吧?还能调别人的插件的命令。那岂不是为所欲为?是的,为所欲为。所以开发起来是很爽的,但是最后还得靠我们自己控制不要瞎搞事情...
添加快捷键
// -- package.json
{
"contributes": {
"commands": [
{
"command": "test-plugin.addConsole",
"title": "add Console"
}
],
"keybindings": [ // 添加快捷键
{
"command": "test-plugin.addConsole", // 命令的标识(ID), 要和 commands 里的命令标识保持一致
"key": "alt+c", // windows 的快捷键
"mac": "alt+c", // mac 的快捷键
"when": "editorFocus" // 什么时候可以触发该快捷键
}
]
},
}
用户自定义属性
有时候,你可能不只需要一个简单的console,你还需要一个辨识度高的console。
比如带颜色的:
再比如带背景色还加前缀的:
而且你可能今天喜欢蓝色,明天就要用红色,或者一天换一个前缀(强行换😂),那最好还是做成可配置的属性(用户自定义属性)。
下面我们就加两个属性,“样式”和“前缀”
添加配置
在 package.json 中添加配置
// -- package.json
{
"contributes": {
"configuration": {
"title": "Add Console Params",
"properties": { // 详细属性
"test-plugin.suffix": { // 属性标识(自定义)
"type": "string",
"default": "【 一个骚骚的前缀 => ", // 属性默认值
"description": "console前缀"
},
"test-plugin.fixStyle": { // 属性标识(自定义)
"type": "string",
"default": "color:#fff;background:#000", // 属性默认值
"description": "前缀样式"
}
}
},
}
}
然后就能在vscode设置文件里找到并且修改了
使用配置
在之前的 addConsole 函数里使用 “用户自定属性”
async function addConsole () {
await vscode.commands.executeCommand('cursorWordLeftSelect')
const editor = vscode.window.activeTextEditor
if(!editor) return;
const textArray = []
const Ranges = editor.selections
// --------- 中间是添加的代码 ---------
// 用”属性标识“ 分别获取属性 “前缀” 和 “样式“
const suffix = vscode.workspace.getConfiguration().get("test-plugin.suffix")
const fixStyle = vscode.workspace.getConfiguration().get("test-plugin.fixStyle")
// --------- 中间是添加的代码 ---------
Ranges.forEach((range) => {
const text = editor.document.getText(range)
let insertText = 'console.log();'
if (text) {
// --------- 这里也要改一下 ---------
// 使用自定义属性 ”前缀“(suffix) 和 ”样式“(fixStyle)
insertText = `console.log('${suffix}${text.replace(/'/g,'"')} : ', '${fixStyle}', ${text});`
}
textArray.push(insertText)
})
vscode.commands.executeCommand('editor.action.insertLineAfter')
.then(()=> {
const editor = vscode.window.activeTextEditor;
const Ranges = editor.selections;
const positionList = []
Ranges.forEach((range, index) => {
const position = new vscode.Position(range.start.line, range.start.character);
positionList.push(position)
})
editor.edit((editBuilder) => {
positionList.forEach((position, index) => {
editBuilder.insert(position, textArray[index]);
})
});
})
}
然后就可以了
一键删除log
功能效果
逻辑
主体逻辑和“add Console”差不多,也是拿到对应的 Range 数组,然后通过Range把console替换成空字符串
- 获取文件的文本
- 正则匹配出所有符合规则的字符串,并通过字符串的index和length重组成Range
- 遍历Ranges,替换console
代码
// -- index.js (入口文件)
const vscode = require('vscode');
function activate(context) {
// 一样的注入 删除log 的命令
let disposable = vscode.commands.registerCommand('test-plugin.deleteConsole', deleteConsole);
context.subscriptions.push(disposable);
}
function deleteConsole () {
// 正则 匹配console.log、console.info等代码,以及log()函数
// 我遇到的特殊场景,log()函数也需要删除。防止大家看到源码会奇怪,所以正则保持和源码一样
const logRegex = /(console.(log|debug|info|warn|error|assert|dir|dirxml|trace|group|groupEnd|time|timeEnd|profile|profileEnd|count)\((.*)\)| log\((.*)\));?/g;
const editor = vscode.window.activeTextEditor
if (!editor) return;
const document = editor.document
const documentText = document.getText()
// 正则匹配的range数组
const Ranges = []
let match;
while (match = logRegex.exec(documentText)) {
// document.positionAt(字符位数) 可以获取到 position
// 再用 position 生成 range
const matchRange = new vscode.Range(document.positionAt(match.index), document.positionAt(match.index + match[0].length))
if (!matchRange.isEmpty) Ranges.push(matchRange);
}
// 删除的方式 和 add Console 其实差不多,拿到range,然后replace掉就行
editor.edit((editBuilder) => {
Ranges.forEach((range, index) => {
editBuilder.replace(range, '');
})
}).then(() => {
// 输出提示: 有多少条 console 被删除
vscode.window.showInformationMessage(`${logStatements.length} console.logs deleted`)
})
}
module.exports = {
activate
}
方案进阶
其实正则去匹配是有一些问题的,一些复杂的log场景就处理不了了。比如:
- log结束,不换行直接调用函数
- log内部放换行的自调用函数
这两个问题其实是正则没办法正确的辨别完整的函数调用,所以我们可以放弃用正则从“字符”的维度解决问题,转向“语法”维度。用虚拟语法树(AST)来找到完整的log函数。
具体的关于AST的内容这里就不细说了,不然那说来就太话长了,而且也没有涉及vscode插件的开发的内容了。有兴趣的同学可以用下面的代码试一下,或者看下看下源码。
代码
// -- index.js (入口文件)
const vscode = require('vscode');
// --------- 中间是添加的代码 ---------
const recast = require("recast");
// --------- 中间是添加的代码 ---------
function activate(context) {
let disposable = vscode.commands.registerCommand('test-plugin.deleteConsole', deleteConsole);
context.subscriptions.push(disposable);
}
function deleteConsole () {
const logRegex = /(console.(log|debug|info|warn|error|assert|dir|dirxml|trace|group|groupEnd|time|timeEnd|profile|profileEnd|count)\((.*)\)| log\((.*)\));?/g;
const editor = vscode.window.activeTextEditor
if (!editor) return;
const document = editor.document
const documentText = document.getText()
const Ranges = []
// --------- 替代正则匹配 ---------
// 将代码解析为抽象语法树(AST)
const ast = recast.parse(documentText)
recast.visit(ast, {
visitExpressionStatement: function(path) { // “函数表达式”的处理回调
const node = path.node
const callee = node.expression.callee;
// 如果 函数调用对象是 "console" 或者 调用函数是 "log"
// 包含了 log() 、 console.log() 、 console.info、 console.warn 等等...
if(callee && ((callee.object && callee.object.name === 'console') || (callee.name === 'log'))){
const _location_ = node.expression.loc
// AST 解析出来的行数 从 1 开始 vscode 处理的行数从 0 开始
const start = new vscode.Position(_location_.start.line - 1, _location_.start.column)
const end = new vscode.Position(_location_.end.line - 1, _location_.end.column)
const endNext = new vscode.Position(_location_.end.line - 1, _location_.end.column + 1)
const nextRange = creatRange(end, endNext)
let matchRange = null;
if(document.getText(nextRange) === ';'){
matchRange = creatRange(start, endNext)
} else {
matchRange = creatRange(start, end)
}
if (!matchRange.isEmpty)
Ranges.push(matchRange);
}
return false
}
})
// --------- 替代正则匹配 ---------
editor.edit((editBuilder) => {
Ranges.forEach((range, index) => {
editBuilder.replace(range, '');
})
}).then(() => {
vscode.window.showInformationMessage(`${logStatements.length} console.logs deleted`)
})
}
module.exports = {
activate
}
结果
源码
【源码戳这里】
为了方便阅读,文章里的代码都写到同一个函数里了,所以和源码有些出入。有什么不对的地方,辛苦在评论区指出~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。