一、前言

很早以前就想写vscode相关的插件,最近有时间于是研究一下。

二、需求

定一个需求:编辑器里面可以用代码注释的形式看小说,达到写代码,看小说两不耽误,老板还会夸你认真工作呢,这是摸鱼的最高境界!

三、成果演示

插件地址:https://marketplace.visualstudio.com/items?itemName=DaHuaZhuXi.read-novel
或者在 vscode 插件栏搜索“小说”,就能找到 ReadNovel 插件了。

在这里插入图片描述
代码注释阅读小说插件:读取txt小说文件,并在当前编辑器的文档第一行使用代码注释的方式插入小说文字,用于阅读。

四、插件开发流程

1、安装环境

npm install -g yo generator-code

2、运行生成器

执行 yo code,按提示填写内容,然后会初始化一个最简单的demo案例

? What type of extension do you want to create? New Extension (JavaScript) //选择语言
? What's the name of your extension? helloworld //插件名称
? What's the identifier of your extension? helloworld //插件标识符,只能小写
? What's the description of your extension? a simple demo //插件描述
? Enable JavaScript type checking in 'jsconfig.json'? Yes //js类型检测
? Initialize a git repository? No //初始化git存储库
? Which package manager to use? npm //包管理器

3、文件介绍

生成插件目录后,需要注意的几个文件有:

package.json // 插件配置
extension.js // 插件程序入口

README.md // 插件描述文件
CHANGELOG.md  // 版本历史描述

package.json 插件配置

{
	// 插件的名字,应全部小写,不能有空格
    "name": "vscode-plugin-demo",
	// 插件的友好显示名称,用于显示在应用市场,支持中文
    "displayName": "VSCode插件demo",
	// 描述
    "description": "VSCode插件demo集锦",
	// 关键字,用于应用市场搜索
    "keywords": ["vscode", "plugin", "demo"],
	// 版本号
    "version": "1.0.0",
	// 发布者,如果要发布到应用市场的话,这个名字必须与发布者一致
    "publisher": "sxei",
	// 表示插件最低支持的vscode版本
    "engines": {
        "vscode": "^1.27.0"
    },
	// 插件应用市场分类,可选值: [Programming Languages, Snippets, Linters, Themes, Debuggers, Formatters, Keymaps, SCM Providers, Other, Extension Packs, Language Packs]
    "categories": [
        "Other"
    ],
	// 插件图标,至少128x128像素
    "icon": "images/icon.png",
	// 扩展的激活事件数组,可以被哪些事件激活扩展,后文有详细介绍
    "activationEvents": [
        "onCommand:extension.sayHello"
    ],
	// 插件的主入口
    "main": "./src/extension",
	// 贡献点,整个插件最重要最多的配置项
    "contributes": {
		// 插件配置项
		"configuration": {
            "type": "object",
			// 配置项标题,会显示在vscode的设置页
            "title": "vscode-plugin-demo",
            "properties": {
				// 这里我随便写了2个设置,配置你的昵称
                "vscodePluginDemo.yourName": {
                    "type": "string",
                    "default": "guest",
                    "description": "你的名字"
                },
				// 是否在启动时显示提示
                "vscodePluginDemo.showTip": {
                    "type": "boolean",
                    "default": true,
                    "description": "是否在每次启动时显示欢迎提示!"
                }
            }
        },
		// 命令
        "commands": [
            {
                "command": "extension.sayHello",
                "title": "Hello World"
            }
        ],
		// 快捷键绑定
        "keybindings": [
            {
                "command": "extension.sayHello",
                "key": "ctrl+f10",
                "mac": "cmd+f10",
                "when": "editorTextFocus"
            }
        ],
		// 菜单
        "menus": {
			// 编辑器右键菜单
            "editor/context": [
                {
					// 表示只有编辑器具有焦点时才会在菜单中出现
                    "when": "editorFocus",
                    "command": "extension.sayHello",
					// navigation是一个永远置顶的分组,后面的@6是人工进行组内排序
                    "group": "navigation@6"
                },
                {
                    "when": "editorFocus",
                    "command": "extension.demo.getCurrentFilePath",
                    "group": "navigation@5"
                },
                {
					// 只有编辑器具有焦点,并且打开的是JS文件才会出现
                    "when": "editorFocus && resourceLangId == javascript",
                    "command": "extension.demo.testMenuShow",
                    "group": "z_commands"
                },
                {
                    "command": "extension.demo.openWebview",
                    "group": "navigation"
                }
            ],
			// 编辑器右上角图标,不配置图片就显示文字
            "editor/title": [
                {
                    "when": "editorFocus && resourceLangId == javascript",
                    "command": "extension.demo.testMenuShow",
                    "group": "navigation"
                }
            ],
			// 编辑器标题右键菜单
            "editor/title/context": [
                {
                    "when": "resourceLangId == javascript",
                    "command": "extension.demo.testMenuShow",
                    "group": "navigation"
                }
            ],
			// 资源管理器右键菜单
            "explorer/context": [
                {
                    "command": "extension.demo.getCurrentFilePath",
                    "group": "navigation"
                },
                {
                    "command": "extension.demo.openWebview",
                    "group": "navigation"
                }
            ]
        },
		// 代码片段
        "snippets": [
            {
                "language": "javascript",
                "path": "./snippets/javascript.json"
            },
            {
                "language": "html",
                "path": "./snippets/html.json"
            }
        ],
		// 自定义新的activitybar图标,也就是左侧侧边栏大的图标
        "viewsContainers": {
            "activitybar": [
                {
                    "id": "beautifulGirl",
                    "title": "美女",
                    "icon": "images/beautifulGirl.svg"
                }
            ]
        },
		// 自定义侧边栏内view的实现
        "views": {
			// 和 viewsContainers 的id对应
            "beautifulGirl": [
                {
                    "id": "beautifulGirl1",
                    "name": "国内美女"
                },
                {
                    "id": "beautifulGirl2",
                    "name": "国外美女"
                },
                {
                    "id": "beautifulGirl3",
                    "name": "人妖"
                }
            ]
        },
		// 图标主题
        "iconThemes": [
            {
                "id": "testIconTheme",
                "label": "测试图标主题",
                "path": "./theme/icon-theme.json"
            }
        ]
    },
	// 同 npm scripts
    "scripts": {
        "postinstall": "node ./node_modules/vscode/bin/install",
        "test": "node ./node_modules/vscode/bin/test"
    },
	// 开发依赖
    "devDependencies": {
        "typescript": "^2.6.1",
        "vscode": "^1.1.6",
        "eslint": "^4.11.0",
        "@types/node": "^7.0.43",
        "@types/mocha": "^2.2.42"
    },
	// 后面这几个应该不用介绍了
    "license": "SEE LICENSE IN LICENSE.txt",
    "bugs": {
        "url": "https://github.com/sxei/vscode-plugin-demo/issues"
    },
    "repository": {
        "type": "git",
        "url": "https://github.com/sxei/vscode-plugin-demo"
    },
	// 主页
    "homepage": "https://github.com/sxei/vscode-plugin-demo/blob/master/README.md"
}

extension.js 插件程序入口

这里只介绍常用的一些方法,具体API请看官网:https://code.visualstudio.com/api/references/vscode-api

// 引入 vscode 模块
const vscode = require('vscode');

/**
 * @description:扩展被激活时执行,只会执行一次
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {

	//注册事件,此事件为:变更当前编辑文档
	vscode.window.onDidChangeActiveTextEditor(() => {
		vscode.window.showInformationMessage('变更当前编辑文档');
	})

	//注册命令helloWorld,命令激活时执行代码(命令要和package.json里面的对应,可以注册多个命令)
	let helloWorld = vscode.commands.registerCommand('helloworld.helloWorld', function () {
		// vscode的消息框
		vscode.window.showInformationMessage('普通消息');
		vscode.window.showWarningMessage('警告消息');
	});

	//注册命令showError
	let showError = vscode.commands.registerCommand('helloworld.showError', function () {
		vscode.window.showErrorMessage('错误消息');
	});

	//订阅命令
	context.subscriptions.push(helloWorld);
	context.subscriptions.push(showError);
}
exports.activate = activate;

// 扩展停用时执行(例如关闭vscode)
function deactivate() {
	console.log("扩展停用");
}

module.exports = {
	activate,
	deactivate
}

4、调试

F5,就能进入调试模式,此时会新开一个vscode窗口,按 ctrl + shift + i 会打chrome的开发者工具,进行更详细的调试。
ps:主编辑器的文件必须以根目录形式打开插件目录,否则按F5无法打开调试窗口


此时在“ 扩展编辑器”上按 ctrl + shift + p ,输入Hello World 则会弹出提示窗口
在这里插入图片描述

5、本地打包和安装

  1. 安装打包工具
    npm i vsce -g
  2. 执行打包
    vsce package
    执行后会在根目录生成 helloworld.vsix 文件
  3. 本地安装
    本地安装只能在插件菜单中选择“从VSIX安装…”
    在这里插入图片描述

6、上线发布

除了本地安装的方式,也可以把你的插件发布到vscode平台,让全世界所有人都可以使用你的插件。

如果你有GitHub账号,可以直接进入第2步,用GitHub账号登录,它会同时创建一个默认组织

  1. 微软账号注册:https://login.live.com/

  2. 创建组织:https://aka.ms/SignupAzureDevOps

  3. 创建令牌:并记下令牌
    在这里插入图片描述
    在这里插入图片描述

  4. 创建开发者
    回到命令行,输入 vsce create-publisher,填入刚才生成的token。
    注意:publisher名称要和package.json里的publisher一致!

  5. 发布插件
    vsce publish
    发布插件后会提供一个插件地址,要过一段时间才能看到你的插件。

  6. 更新插件
    只需更新 package.json 里的 version 版本号,然后执行 vsce publish 就可以更新了。

五、附【代码注释阅读小说】核心代码

/*
 * @Author: DaHuaZhuXi
 * @Date: 2020-11-02 16:47:19
 * @LastEditTime: 2020-11-05 20:00:41
 * @LastEditors: DaHuaZhuXi
 * @Description: 主程序文件
 */

const vscode = require('vscode');
const fs = require("fs");
let fileData = ""; //读取文件后的字符数据
let datas = []; //通过lineBreak分割后的字符数组
let curPage = 0; //当前datas索引(分页记录)
const tipTxt = "【阅读进度,勿删!Reading progress, do not delete!】"; //文件第一行存储阅读进度文字

const lineBreak = "\r\n"; //分页使用的分割字符
let filePath = ""; //文件路径
let replaceMark = "/*$*/"; //替换标记

/**
 * @param {vscode.ExtensionContext} context
 * @Description: 插件激活时调用
 */
function activate(context) {

	//变更当前激活文件时触发(用于记录阅读进度)
	vscode.window.onDidChangeActiveTextEditor(() => {
		saveReadProgress();
	})

	//读取文件并插入注释
	let disposable = vscode.commands.registerCommand('ReadNovel.ReadNovel', function () {
		fileData = ""; 
		datas = []; 
		curPage = 0; 
		filePath = vscode.workspace.getConfiguration().get('ReadNovel.filePath').replace(/(^\s*)|(\s*$)/g, "");
		replaceMark = vscode.workspace.getConfiguration().get('ReadNovel.replaceMark');

		//文件存在检测
		if (filePath === "") {
			vscode.window.showErrorMessage('文件不能存在,请到设置里添加文件地址');
			return;
		};

		//文件类型检测
		if (getFileType(filePath) != "txt") {
			vscode.window.showErrorMessage('文件类型不正确,只支持utf-8编码的txt文件');
			return;
		};

		//打开文件
		try {
			fileData = fs.readFileSync(filePath, 'utf-8');
		} catch (error) {
			vscode.window.showErrorMessage(error.toString());
			return
		}

		curPage = getSavePage(fileData);
		datas = fileData.split(lineBreak);
		console.log("过滤前段落长度:", datas.length);
		datas = datas.filter(str => isEmptyLine(str))
		console.log("过滤后段落长度:", datas.length);

		//在当前文档第一行插入注释
		handleTxt({ action: "insert", position: [0, 0], data: datas[curPage] })
	});

	//下一页
	let nextPage = vscode.commands.registerCommand('ReadNovel.ReadNovel_nextPage', function () {
		handleTxt({ action: "replace", position: [1, 0], data: datas[++curPage] })
	});

	//上一页
	let prevPage = vscode.commands.registerCommand('ReadNovel.ReadNovel_prevPage', function () {
		handleTxt({ action: "replace", position: [1, 0], data: datas[--curPage] })
	});

	//清空注释(老板键)
	let clear = vscode.commands.registerCommand('ReadNovel.ReadNovel_clear', function () {
		handleTxt({ action: "replace", position: [1, 0], data: "" })
	});

	context.subscriptions.push(disposable);
	context.subscriptions.push(nextPage);
	context.subscriptions.push(prevPage);
	context.subscriptions.push(clear);
}

/**
 * @description: 编辑器第一行文字处理
 * @param {Object} option
 * @param {String} option.action //操作
 * @param {Array} option.position //位置
 * @param {String} option.data //数据
 */
function handleTxt(option) {
	if (!isLegal()) return;
	const editor = vscode.window.activeTextEditor;
	editor.edit(editBuilder => {
		let result = replaceMark.replace("$", option.data) + "\r\n";
		switch (option.action) {
			case "insert":
				editBuilder.insert(new vscode.Position(option.position[0], option.position[1]), result)
				break;
			default:
				editBuilder.replace(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(option.position[0], option.position[1])), result)
				break;
		}
	})
}

//获取文件类型
function getFileType(filePath) {
	var startIndex = filePath.lastIndexOf(".");
	if (startIndex != -1) {
		return filePath.substring(startIndex + 1, filePath.length).toLowerCase();
	} else {
		return "";
	}
}

//存储阅读进度
function saveReadProgress() {
	if (!isLegal()) return;
	let datas = fileData.split("\r\n");
	if (datas.length === 0) return;
	//如果没有记录进度则添加进度
	if (datas[0].indexOf(tipTxt) === -1) {
		let newData = curPage + tipTxt + "\r\n" + fileData;
		writeFile(newData)
	} else { //如果有进度则变更进度
		if (curPage === 0) return;
		let savePage = Number(datas[0].split(tipTxt)[0]);
		if (curPage === savePage) return;
		let oldLine = savePage + tipTxt + "\r\n";
		let newLine = curPage + tipTxt + "\r\n";
		let newData = fileData.replace(oldLine, newLine);
		writeFile(newData)
	}
}

//检测合法性
function isLegal() {
	if (datas.length === 0 || curPage < 0 || curPage > datas.length) return false;
	return true;
}


//写入文件
function writeFile(data) {
	fs.writeFile(filePath, data, { encoding: 'utf8' }, function (err) {
		if (err) return console.error(err);
	});
}

//获取阅读进度
function getSavePage(fileData) {
	const datas = fileData.split("\r\n")
	if (datas[0].indexOf(tipTxt) === -1) return curPage;
	let savePage = Number(datas[0].split(tipTxt)[0]);
	return savePage
}

//检测是否空行
function isEmptyLine(str) {
	return str.replace(/[ ]|[\r\n]/g, "") != "";
}

exports.activate = activate;

//插件销毁时调用
function deactivate() {
	saveReadProgress();
}

module.exports = {
	activate,
	deactivate
}

六、参考资料

  1. VSCode插件开发全攻略 https://www.cnblogs.com/liuxianan/p/vscode-plugin-overview.html
  2. VSCode官方文档 https://code.visualstudio.com/api
  3. VScode API https://code.visualstudio.com/api/references/vscode-api

兄弟,点个赞再走!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐