「 VSCode插件开发 」一键注释术!提高你的工作效率

2,959 阅读4分钟

相信你也有这样的感受,在工作中,有时候会被一些简单重复的操作所影响效率,这个插件查了不少资料,也趟过一些坑,现在终于完成了一个初版,将代码贴出来供给大家参考。
PS: 全英文文档对我来说真的是有点不友好

vscode插件开发主要流程介绍(正文请下滑)

VSCode插件开发文档
微软为了方便vscode插件开发,有推出一个vscode的脚手架,通过此脚手架可以快速开发,可以通过以下命令安装:

npm install -g yo generator-code


安装好之后,输入 yo code 初始化项目,会出现提示选择语言,在我的项目中,我选择的是js,这里就不进行贴图了。

extension.js文件:

extension.js是一个入口文件,在这个文件中,我们可以看到有两个方法,分别是 activate和deactivate activate: 在插件被激活的时候会调用此方法(在package.json中的activationEvents来定义插件被激活的条件),我们可以在这个方法中去对package.json的方法来进行注册

function activate(context){
   let disposable = vscode.commands.registerCommand('start-plug.helloWorld', () => {
    //此处填入该命令被执行时候所需执行的代码(建议封装单独js文件)
});  
}

deactivate:当扩展被停用时,将调用此方法,可以选择在此处加入一些友好提示

function deactivate(){ }

package.json文件:

package.json可以对插件进行一些配置,主要字段如下:
activationEvents: 用来定义插件激活的事件,或者说是条件,可以是文件后缀名,或者是命令等。 取值可以是

  • onLanguag
  • onCommand
  • onDebug
  • onDebugInitialConfigurations
  • onDebugResolve
  • workspaceContains
  • onFileSystem
  • onView
  • onUri
  • ......

具体参考Activation Events声明文档


"activationEvents": [
    "onStartupFinished"
]

main
用来定义入口文件的路径

contributes:
contributes可以看作一种声明
比如contributes的commands,在文档中的这段话有体现出来:"当调用命令时(从键绑定、从命令面板、任何其他菜单或以编程方式),VS Code 将发出 activationEvent onCommand:${command}";也就是说,在接收到用户端发出的commands(指令)的时候,会去activationEvent中找commands中的符合用户输入的command指令的行为,找到后执行,默认在命令面板( Ctrl+Shift+P ) 中;

而menus: 用来定义当鼠标选中某个文件的时候,所展示出来的菜单可选项,而menu中的command,表示选中该菜单栏时,所需要执行的命令,菜单栏的title,对应的是menu中的command在commands中的title

  • breakpoints
  • colors
  • commands
  • customEditors
  • debuggers
  • keybindings
  • languages
  • menus
  • ........

具体列表参见:contributes声明文档


正文

        现在接手的这个项目有些特殊,在生产环境下的时候需要将调用了某方法的代码注释掉,否则会导致js文件报错,而在预发布环境下,又需要将之前注释掉的方法恢复回来,令其重新生效。这就导致了每次在迭代的时候,都需要在js文件中去搜索这个方法然后一个个注释,预发布时再一个个取消注释,非常麻烦。
        作为码农,怎么甘心接受这种低级而又重复的操作呢!于是我想着能否开发一个vsCode插件,来帮助我遍历、查找、替换,同时既然是插件,肯定是不能只针对我接手的这个项目进行开发,为了兼容日后更多的情况,可以采用文本框输入的方式来确定所需要注释掉的代码。撸起袖子,说干就干!

在脚手架中使用javascript创建的目录:

image

首先来看看入口文件extension.js

其中所用到的FileHanding是自己创建的文件,建议将处理文件的方法单独封装成一个或几个js;

// The module 'vscode' contains the VS Code extensibility API模块'vscode'包含VS Code扩展API  
// Import the module and reference it with the alias vscode in your code below导入模块并使用下面代码中的别名vscode引用它  
const vscode = require('vscode');
const FileHanding = require('./fileHanding')
// fileHanding属于自定义的js方法
/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
  console.log('context',context);
  //TODO : 在package.json中的activationEvents来定义插件被激活的条件 
  // 这一行代码只会在你的扩展被激活时执行一次  
  console.log('Congratulations, your extension "start-plug" is now active!');
  vscode.window.showInformationMessage('插件已被激活');
  // 该命令已经在包中定义json文件  
  // 在package中定义的事件你需要在此处注册
  // 参数必须与package.json中的command字段匹配  
  let codeClear = vscode.commands.registerCommand('custom-code.clear', (params) => {
  //调用vscode的输入框API
    vscode.window.showInputBox({
      password: false,
      placeHolder: '请输入需要将其注释的内容(逐行匹配,匹配开头,忽略空格)',
    }).then(msg => {
    // 这部分异步回调会在输入框出现的时候执行一次,此时的msg值为undefined,所以需要过滤掉此次执行,对msg值进行判断
      if (msg) {
        const fileOptions = {
          encoding: 'utf-8'
        };
        FileHanding.handleMain(params.fsPath, fileOptions,`${msg}`).then(res => {
          vscode.window.showInformationMessage('文件已更新,可以进行查看咯');
        },err => {
          vscode.window.showErrorMessage(err)
        })
      }
    })
  });

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

// 当您的扩展被停用时,将调用此方法  
function deactivate() {}

module.exports = {
  activate,
  deactivate
}

这是用于处理文件内容的fileHanding.js文件:在这个文件中我引入了node的fs模块,对文件进行读取和写入.

//TODO:想做一个类似日志的txt文件,用来保存每次更改的记录,可是面对mac和windows的目录差异,如何确定放置位置?
//TODO: 是否需要增加类型过滤?
const FileSystem = require('fs');

function isFolder(path) { //判断是否是文件夹
  return FileSystem.lstatSync(path).isDirectory();
}
function handleMain(path, options, msg){
  return new Promise((resolve,reject) => {
    if (isFolder(path)) {//如果是文件夹,开始递归,更改文件夹下所有文件
      const filePathList = FileSystem.readdirSync(path);
      console.log('文件列表',filePathList,path);
      if (filePathList.length) {
        for (let index = 0; index < filePathList.length; index++) {
          const childPath = `${path}/${filePathList[index]}`;
          handleMain(childPath,options, msg)
        }
      }else{
        reject('文件夹为空')
      }
    }else{
      const result = readFile(path, options, msg)
      result.state ? resolve() : reject(result.errCode);
    }
  })
}
function readFile(path, options, msg) {
    if (!path || !msg) {
      return {
        state: false,
        errCode: '当前状态异常'
      }
    }
    if (!FileSystem.existsSync(path)) {// 判断文件是否存在
      return {
        state: false,
        errCode: '现有路径没有找到文件'
      }
    }
    try {
      const fileData = FileSystem.readFileSync(path, options);
      const fileCodeList = fileData.toString().split('\n'); //将Buffer转为string类型后以行为单位分割
      clearHandleFile(path, fileCodeList, options, msg); // 路径、代码行数组、字符类型、用户输入文本
      return {
        state: true
      }
    } catch (error) {
      return {
        state: false,
        errCode: `未知错误${error}`
      }
    }
}

function clearHandleFile(path, fileStringList, options, msg) {
  const handleCodeList = [];
  let isUpdate = false;
  for (let index = 0; index < fileStringList.length; index++) {
    const element = fileStringList[index];
    if (isAccordCondition(element.replace(/(^\s*)/g, ""), msg)) { //去除左边空格
      const insertIndex = element.search(msg);
      const handleString = `${element.slice(0,insertIndex)}// ${element.slice(insertIndex)}`;
      handleCodeList.push(handleString);
      isUpdate = true;
    } else {
      handleCodeList.push(element)
    }
  }
  if (isUpdate) { //如果该文件进行了处理则重新写入,没有变更的话不写入
    const handleFileStr = handleCodeList.join('\n');
    FileSystem.writeFileSync(path, handleFileStr, options);
  }
}

function isAccordCondition(str, msg) {
  //TODO: 此处判断注释太过单一,后续需将/*   */考虑在内
  if (/^\/\//.test(str)) { //如果是注释则返回false,不做改变
    return false
  }
  let exp = RegExp(`^${msg}`); //采用输入框动态传入,匹配开头
  return exp.test(str);
}
module.exports = {
  readFile,
  handleMain
}

我们可以来看一下 package.json文件的内容:
contributes声明文档上面已发,此处我是用的激活条件是onStartupFinished

// 此处我是用的激活条件是onStartupFinished,此激活事件会在VS Code
启动后的一段时间内发出,感兴趣的扩展将被激活。这与*激活事件类似,但不会减慢 VS Code 的启动速度。目前,此事件在所有*激活的扩展完成激活后发出。
{
	"name": "start-plug",
	"displayName": "yln-plug",
	"description": "private plug",
	"version": "0.0.1",
	"engines": {
		"vscode": "^1.60.0"
	},
	"categories": [
		"Other"
	],
	"activationEvents": [
		"onStartupFinished"
	],
	"main": "./extension.js",
	"contributes": {
		"commands": [
			{
				"command": "custom-code.clear",
				"title": "code-clear"
			}
		],
		"menus": {
			"explorer/context": [
				{
					"command": "custom-code.clear",
					"when": "filesExplorerFocus",
					"group": "navigation@1"
				}
			]
		}
	},
	"scripts": {
		"lint": "eslint .",
		"pretest": "npm run lint",
		"test": "node ./test/runTest.js"
	},
	"devDependencies": {
		"@types/vscode": "^1.60.0",
		"@types/glob": "^7.1.4",
		"@types/mocha": "^9.0.0",
		"@types/node": "14.x",
		"eslint": "^7.32.0",
		"glob": "^7.1.7",
		"mocha": "^9.1.1",
		"typescript": "^4.4.3",
		"@vscode/test-electron": "^1.6.2"
	}
}



最终实现结果

         按F5即可启动插件进行调试,然后在mac上用command+shift+P调出命令行,输入所配置的命令即可,因为我在package.json中配置了menu,所以对文件直接右键即可

image.png image.png 在弹出框输入string后 image.png

至此,该插件开发已经告一段落,平时可以用于在项目上线前,将console.log清理一下,或是根据业务需要进行一些批量操作,当然,在这里面还有很多不完善的地方,比如:

  • 暂时不支持/* */格式的注释匹配
  • 只能以行为单位匹配每行代码的开头
  • 没有一键恢复被注释代码的功能
  • 我一直想做个更改日志,还没有行动...
  • .......

怎么说呢,这只是第一个版本,如果后续业务需要,或者兴趣使然,我也会找个时间将它继续拓展、完善功能,因为最难的一步我们已经走完了↓

种一棵树最好的时间是十年前,其次是现在。

另外,在插件的开发过程中,借鉴了寒草大神的思路,加上自己的一些小想法,促进了这个插件以及这篇博文的诞生,也算是自己一次较为创新的尝试吧。在插件开发并测试完成的那一刻,我深吸一口气,你看,空气中都弥漫着成就感。

特别鸣谢:寒草:「教你用十分钟开发一款提升工作体验的vscode插件🌿 」console, debugger一键删除|自定义代码模板