前言
前段时间做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 = 1let 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}
后果
源码
【源码戳这里】
为了不便浏览,文章里的代码都写到同一个函数里了,所以和源码有些出入。有什么不对的中央,辛苦在评论区指出~