写注释真的好烦,每次都得/**......*/的形式才有jsDoc的效果,真的不想浪费时间了,于是写个vscode插件,添加一下jsDoc注释,提升点效率
1.vscode插件开发脚手架
使用Yeoman脚手架工具和generator-codeVS Code 扩展生成器来生成一个vscode插件开发项目
sh
# 安装
npm install -g yo generator-code
# 执行脚手架,生产项目
yo code

可以看到有不同的类型
- New Extension (TypeScript):基础ts插件开发项目
- New Extension (JavaScript):基础js插件开发项目
- New Color Theme:主题颜色配置插件开发项目
- New Language Support:程序语义支持插件开发
- New Code Snippets:代码片段插件开发
- New Keymap:快捷键插件开发
- New Extension Pack:插件包开发
- New Language Pack (Localization):语言包插件
- New Web Extension (TypeScript):网页插件开发,打开一个新页面,如图片预览
- New Notebook Renderer (TypeScript):笔记本渲染插件开发,如代码和Markdown的格式化,交互小程序
小试牛刀的话只需要选择简单的New Extension (TypeScript)+esbuild
接下来只需要根据自己的情况填写插件项目名称,插件标识,插件描述等
2.运行调试第一个项目
选择最基础的Typescript模板,建议使用yarn管理包,后面打包成vscode插件包的时候pnpm会因为文件找不到而失败。

package.json配置命令,可以通过Ctrl+Shift+P唤起vscode命令栏搜索命令名称Hello World
json
"contributes": {
"commands": [
{
"command": "vscode-xcomment.helloWorld",
"title": "Hello World"
}
]
},
src/extension.ts文件对应注册命令的操作
ts
import * as vscode from "vscode";
//安装的时候
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "vscode-hello" is now active!');
//注册命令
const disposable = vscode.commands.registerCommand("vscode-hello.helloWorld", () => {
//触发命令后执行
//右下角弹出信息框
vscode.window.showInformationMessage("Hello World from vscode-hello!");
cconsole.log("hello", {name: "vscode", say: "hello world", age: 123});
});
context.subscriptions.push(disposable);
}
//卸载的时候
export function deactivate() {}
首次运行Debug vscode插件会提示有问题,原因是因为launch.json配置了预运行的任务"preLaunchTask": "${defaultBuildTask}",即在tasks.json里面配置的预运行任务,其中有个npm: watch:esbuild的任务有问题。
- 解决方案1:安装插件
esbuild Problem Matchers,重新打开在debug - 解决方案2:把
preLaunchTask去掉,手动执行命令npm run watch监听代码改变并编译成js,在运行debug



Debug时会弹出一个新的vscode窗口,通过Ctrl+Shift+P快捷键唤起vscode命令栏或者右下角设置里面打开命令栏,可以搜索到命令名称Hello World,点击执行就可以看到弹出信息框的内容。


同时我们也能在vscode-hello项目的DEBUG CONSOLE调试控制台看到相关的输出打印

3.添加快捷键和右击菜单
在package.json添加contributes.keybindings配置快捷键
json
"contributes": {
"keybindings": [
{
"command": "vscode-hello.helloWorld",
"key": "alt+H"
}
]
}
在package.json添加contributes.menus.editor/context配置编辑器中右击菜单
json
"contributes": {
"editor/context": [
{
"command": "vscode-hello.helloWorld",
"group": "1_modification@100",
"alt": "vscode-hello.helloWorld",
"key": "alt+H"
}
]
}

可以直接通过右击菜单的Hello World或者Alt+H快捷键触发helloWorld命令
如果想将菜单放在别的地方或者别的分组group,可以查看官方文档contributes.menus的配置 vscode.js.cn/api/referen...

当然可以添加一些快捷键和菜单生效的条件设置,比如当前打开的代码是ts/js/vue文件才出现或生效
json
"keybindings": [
{
"command": "vscode-xcomment.add",
"when": "editorTextFocus && resourceFilename =~ /.(js|ts|vue|jsx|tsx)$/",
"key": "alt+/"
},
],
"menus": {
"editor/context": [
{
"command": "vscode-xcomment.add",
"when": "editorTextFocus && resourceFilename =~ /.(js|ts|vue|jsx|tsx)$/",
"group": "1_modification@102",
"alt": "vscode-xcomment.comment",
"key": "alt+/"
}
]
}
具体的when子句上下文配置请看官方文档vscode.js.cn/api/referen...
4.给ts/js/vue文件添加注释
vscode获取当前打开文档的代码
ts
const editor = vscode.window.activeTextEditor;
const doc = editor.document;
const fileName = doc.fileName;//文件绝对路径
const code = doc.getText();//代码内容
vscode检查是否有语法错误
执行命令前先进行语法错误判断,如果没有错误再执行
ts
export function checkError(editor: vscode.TextEditor) {
const diagnostics = vscode.languages.getDiagnostics(editor.document.uri);
const hasSyntaxError = diagnostics.some(
(d) =>
d.severity === vscode.DiagnosticSeverity.Error &&
(/syntax|unexpected|expected/i.test(d.message) ||
(d.code && typeof d.code === "string" && d.code.toLowerCase().includes("syntax")))
);
if (hasSyntaxError) {
return true;
}
return false;
}
vscode判断文件类型
限定执行命令的文件类型
ts
function checkFile(editor: vscode.TextEditor) {
const doc = editor.document;
const fileName = doc.fileName;
if (/\.(ts|js|vue|jsx|tsx)$/.test(fileName)) {
return true;
}
return false;
}
注册命令并提示信息
ts
const disposable = vscode.commands.registerCommand(PREFIX + "comment", () => {
//触发命令后执行
//获取当前打开的编辑页面
const editor = vscode.window.activeTextEditor;
if (editor) {
//检查是否有语法错误
if (checkError(editor)) {
//右下角弹出错误提示信息
vscode.window.showErrorMessage("语法错误是不执行添加注释的命令!");
return;
}
//检查文件类型是否正确
if (!checkFile(editor)) {
vscode.window.showErrorMessage("文件必须是js/ts/vue");
return;
}
//添加注释
const ctrl = new AddCommentController(editor);
ctrl.doAction();
ctrl.clearAll();
}
});
context.subscriptions.push(disposable);
获取vscode当前光标所在位置
ts
editor.selection.active
editor.selection.active.line//光标所在行
editor.selection.active.character//光标所在该行的第几个字符的位置
由于vscode按行来记录光标位置,所以为了方便找到具体字符位置,将代码按行进行分割,并进行索引开始结束位置和内容记录
ts
getSourceLines(code: string) {
const list: Array<[number, number, string]> = [];
const lines = code.split("\n");
if (lines.length) {
let pre = 0;
lines.forEach((line, idx) => {
//+idx是因为换行号也算一个字符,需要加上
list.push([pre + idx, pre + idx + line.length, line]);
pre += line.length;
});
}
return list;
}
光标位置
- 判断是否有光标,即focus聚焦在该代码编辑上了,有时候打开文档但是没有聚焦光标
ts
const doc = this.editor.document;
const fileName = doc.fileName; //文件绝对路径
const code = doc.getText(); //代码内容
this.sourceLines = this.getSourceLines(code);
//是否有光标
if (!this.editor.selection.active) {
return;
}
const pos = this.editor.selection.active.line;
const item = this.sourceLines[pos];
//判断光标范围在文档代码有效范围内
if (!item) {
return;
}
//光标具体所在代码的字符索引位置
const p = item[0] + this.editor.selection.active.character;
- 如果是vue文件,要判断光标是否定位在vue的js/ts代码范围内,再获取其中的js/ts代码
ts
if (fileName.endsWith(".vue")) {
let startIndex = code.indexOf("<script");
let endIndex = code.indexOf("</script>");
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
return;
}
for (let i = startIndex; i < endIndex; i++) {
const c = code[i];
if (c === ">") {
startIndex = i + 1;
break;
}
}
if (p < startIndex || p > endIndex) {
vscode.window.showInformationMessage("vue文件光标位置不在js/ts范围内");
return;
}
//vue文件内js/ts代码
const script = code.substring(startIndex, endIndex);
}
ts/js解析代码成AST
我们常用Typescript库校验和编译代码,同时它也能将代码解析成AST
ts
import * as ts from "typescript";
const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
node打印出来可能很难清楚其结构,可以到astexplorer查看具体的AST树

遍历代码根节点下所有节点,查看节点类型和内容
ts
sourceFile.statements.forEach((node) => {
//查看节点类型
console.log(ts.SyntaxKind[node.kind]);
console.log(node.getText());
console.log("------");
});

查找光标位置的节点
遍历AST根据节点范围判断光标是否在该节点,然后深度遍历该节点,直到找到最终的子节点,即光标所在具体位置,期间可以收集所有父子节点。
因为每类节点的结构都有所差异,推荐使用ts自带的ts.forEachChild遍历子节点的方法
ts
findNode(file: ts.SourceFile, pos: number) {
let result: ts.Node[] = [];
const visitNode = (node: ts.Node) => {
try {
ts.forEachChild(node, (child) => {
if (pos >= child.getStart() && pos < child.getEnd()) {
result.push(child);
//深度遍历子节点
visitNode(child);
//跳出循环
throw Error();
}
});
} catch (error) {}
};
for (let i = 0; i < file.statements.length; i++) {
const it = file.statements[i];
if (pos >= it.getStart() && pos < it.getEnd()) {
result.push(it);
//深度遍历子节点
visitNode(it);
break;
}
}
return result;
}
获取当前光标所在的节点
ts
const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
const currentNodes = this.findNode(sourceFile, p);
给不同节点添加注释
- 判断该节点是否已有jsDoc注释,如果有则不添加注释。
- 如果是单行注释或者非jsDoc的多行注释则转化为jsDoc注释
- 如果没有注释则直接添加
ts
checkDocs(node: ts.Node, sourceFile: ts.SourceFile, cb: (msg?: string[]) => void) {
//@ts-ignore
if (node.jsDoc && node.jsDoc.length > 0) {
//有jsDoc就不添加注释
} else {
const comments: string[] = [];
//头部注释
const leadingComments = ts.getLeadingCommentRanges(sourceFile.text, node.pos);
if (leadingComments) {
leadingComments.forEach((comment) => {
const s = sourceFile.text.substring(comment.pos, comment.end);
comments.push(s.replace(/[\*\/]+/g, ""));
});
}
//尾部注释
const tailingComments = ts.getTrailingCommentRanges(sourceFile.text, node.end);
if (tailingComments) {
tailingComments.forEach((comment) => {
const s = sourceFile.text.substring(comment.pos, comment.end);
comments.push(s.replace(/[\*\/]+/g, ""));
});
}
if (comments && comments.length > 0) {
//将旧的注释添加到jsDoc内
cb(comments);
} else {
//添加新的注释
cb();
}
}
}
如果已有注释则延用,如果没有注释则获取节点的名称作为注释内容
ts
getNodeName(stmt: ts.Node, msg?: string[]) {
const comments: string[] = [];
if (msg) {
msg.forEach((a) => {
if (!/^\s+$/.test(a)) {
comments.push(" * " + a);
}
});
}
if (comments.length === 0) {
//获取父级节点名称
let current: ts.Node = stmt;
while (current) {
//@ts-ignore
let name = stmt.name;
if (name) {
const n = name.getText();
if (!/^\s+$/.test(n)) {
comments.push(" * " + n);
break;
}
}
current = current.parent;
}
}
//如果父级没有名称则添加默认注释
if (comments.length === 0) {
comments.push(` * description`);
}
return comments;
}
普通函数与方法
普通函数声明
ts
//对应节点类型 FunctionDeclaration
function sum(a: number, b: number): number {
return a + b;
}
方法定义
ts
const obj = {
//对应节点类型 MethodDeclaration
fun(msg: string) {
console.log(msg);
}
};
class Person {
//对应节点类型 ConstructorDeclaration
constructor(aaa: string) {
console.log(aaa);
}
//对应节点类型 MethodDeclaration
dd(dd: string) {
console.log(dd);
}
}
Type和Interface函数定义
ts
interface Shape {
//对应节点类型 MethodSignature
draw(x: number, y: number): void;
}
type DrawType = {
//对应节点类型 MethodSignature
draw(x: number, y: number): void;
};
符合此函数结构就添加注释
ts
if (
ts.isFunctionDeclaration(stmt) ||
ts.isMethodDeclaration(stmt) ||
ts.isMethodSignature(stmt) ||
ts.isConstructorDeclaration(stmt)
) {
this.checkDocs(stmt, sourceFile, this.addDocFun(stmt));
return;
}
获取函数参数变量和返回类型的jsDoc注释
ts
getFunComments(
stmt:
| ts.FunctionDeclaration
| ts.MethodDeclaration
| ts.ArrowFunction
| ts.FunctionExpression
| ts.ConstructorDeclaration
| ts.MethodSignature
): string[] {
const comments: string[] = [];
//参数
if (stmt.parameters) {
stmt.parameters.forEach((param) => {
comments.push(
` * @param ${param.type ? `{${param.type.getText().replace(/\s/g, "")}}` : "{any}"} ${param.name.getText().replace(/\s/g, "")} - description`
);
});
}
//返回值
if (!ts.isConstructorDeclaration(stmt) && stmt.type && stmt.type.kind !== ts.SyntaxKind.VoidKeyword) {
comments.push(` * @returns {${stmt.type.getText().replace(/\s/g, '') || 'any'}} description`);
}
return comments;
}
返回给普通函数添加注释的回调
ts
addDocFun(stmt: ts.FunctionDeclaration | ts.MethodDeclaration | ts.ConstructorDeclaration | ts.MethodSignature) {
return (msg?: string[]) => {
const comments = this.getNodeName(stmt, msg);
comments.push(...this.getFunComments(stmt));
this.addNodeComment(stmt, comments);
};
}
箭头函数与匿名函数
箭头函数
ts
//对应的AST结构 VariableDeclaration.initializer:ArrowFunction
const myFun = (a: number, b: number): number => {
return a + b;
};
给变量赋值匿名函数
ts
//对应的AST结构 VariableDeclaration.initializer:FunctionExpression
const myFun = function (a: number, b: number): number {
return a + b;
};
符合此结构就添加注释
ts
if (
ts.isVariableDeclaration(declaration) &&
declaration.initializer &&
(ts.isFunctionExpression(declaration.initializer) || ts.isArrowFunction(declaration.initializer))
) {
this.checkDocs(declaration, sourceFile, this.addInitializerDoc(stmt, declaration.initializer));
return;
}
返回给箭头函数和匿名函数添加注释的回调
ts
addInitializerDoc(stmt: ts.Node, initializer: ts.FunctionExpression | ts.ArrowFunction) {
return (msg?: string[]) => {
const comments = this.getNodeName(initializer, msg);
comments.push(...this.getFunComments(initializer));
this.addNodeComment(stmt, comments);
};
}
同理对象或类属性的箭头函数与匿名函数赋值
ts
const obj = {
//对应的AST结构 PropertyAssignment.initializer:ArrowFunction
aa: (msg: string) => {
console.log(msg);
},
//对应的AST结构 PropertyAssignment.initializer:FunctionExpression
bb: function (ccc: string) {
console.log(ccc);
}
};
class Person {
//对应的AST结构 PropertyDeclaration.initializer:ArrowFunction
aa = (msg: string) => {
console.log(msg);
};
//对应的AST结构 PropertyDeclaration.initializer:FunctionExpression
bb = function (ccc: string) {
console.log(ccc);
};
}
ts
if (
(ts.isPropertyAssignment(stmt) || ts.isPropertyDeclaration(stmt)) &&
stmt.initializer &&
(ts.isFunctionExpression(stmt.initializer) || ts.isArrowFunction(stmt.initializer))
) {
this.checkDocs(stmt, sourceFile, this.addInitializerDoc(stmt, stmt.initializer));
return;
}
给属性等添加注释
ts
//对应节点类型 TypeAliasDeclaration
type AAA={
//对应节点类型 PropertySignature
aaa:string;
}
//对应节点类型 InterfaceDeclaration
interface BBB {
//对应节点类型 PropertySignature
bbb:string;
}
//对应节点类型 ClassDeclaration
class CCC{
//对应节点类型 PropertyDeclaration
ccc:string='hello';
}
符合该节点类型的添加注释
ts
if (ts.isInterfaceDeclaration(stmt) || ts.isClassDeclaration(stmt)) {
this.checkDocs(stmt, sourceFile, this.addDocProp(stmt));
return;
} else if (ts.isTypeAliasDeclaration(stmt) && stmt.type && ts.isTypeLiteralNode(stmt.type)) {
this.checkDocs(stmt, sourceFile, this.addDocProp(stmt));
return;
} else if (ts.isPropertyDeclaration(stmt) || ts.isPropertySignature(stmt) || ts.isPropertyAssignment(stmt)) {
this.checkDocs(stmt, sourceFile, this.addDocProp(stmt));
return;
}
返回属性等节点注释回调
ts
addDocProp(prop: ts.Node) {
return (msg?: string[]) => {
const comments = this.getNodeName(prop, msg);
this.addNodeComment(prop, comments);
};
}
变量等于函数运行结果
ts
//对应的AST结构 VariableDeclaration.initializer=CallExpression
const state = reactive({
aaa: 1
});
//对应的AST结构 VariableDeclaration.initializer=CallExpression
const valRef = ref("hello");
符合结构添加注释
ts
if (ts.isVariableDeclaration(declaration) && declaration.initializer && ts.isCallExpression(declaration.initializer)) {
this.checkDocs(stmt, sourceFile, this.addDocProp(stmt));
return;
}
其他节点添加注释
ts
addComment(sourceFile: ts.SourceFile, nodes: ts.Node[]) {
if (nodes.length) {
for (let i = nodes.length - 1; i >= 0; i--) {
const stmt = nodes[i];
// if ...
}
//其他节点添加注释
const stmt = nodes[nodes.length - 1];
this.checkDocs(stmt, sourceFile, this.addDocProp(stmt));
}
}
this.addComment(sourceFile, currentNodes);
插入注释内容
使用ts库插入注释
给节点插入头部注释
ts
addNodeComment(node: ts.Node, comments: string[]) {
ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, "*" + comments.join("\n"), true);
}
获取新的代码打印内容
ts
printCode(sourceFile: ts.SourceFile) {
const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
const printed = printer.printFile(sourceFile);
return printed;
}
替换ts/js来添加注释
ts
if (fileName.endsWith(".vue")) {
//...
const code = this.printCode(sourceFile);
//vue文件替换js/ts部分
const newText = text.substring(0, startIndex) + "\n" + code + text.substring(endIndex);
this.replaceAllText(newText);
} else {
//...
const newText = this.printCode(sourceFile);
this.replaceAllText(newText);
}
替换全文
ts
replaceAllText(printed: string) {
const editor = this.editor;
editor.edit((editBuilder) => {
const firstLine = editor.document.lineAt(0);
const lastLine = editor.document.lineAt(editor.document.lineCount - 1);
const textRange = new vscode.Range(firstLine.range.start, lastLine.range.end);
editBuilder.replace(textRange, printed);
});
}
以上方法不推荐,因为printer会将空格之类的格式去掉,会导致prettier之类的格式化被去掉,git对比出大量代码已修改
vscode文本插入
记录需要插入的注释内容,因为只有一处添加注释,然后就会停止遍历光标所在位置的父子节点
ts
addNodeComment(node: ts.Node, comments: string[]) {
const c = "/**" + comments.join("\n") + "*/";
this.comment = c;
}
插入文本,获取光标所在行的文本,判断是否为空白字符,如果全是空白字符则直接插入,否则按照当行前面空格位置插入
ts
//该行内容
const linestr = this.sourceLines[pos][2];
if (/^\s*$/.test(linestr)) {
//如果全是空白字符则直接插入
this.editor.edit((editBuilder) => {
editBuilder.insert(this.editor.selection.active, this.comment);
});
} else {
//非空白字符,按照当行前面空格位置插入
const spaces: string[] = [];
for (let i = 0; i < linestr.length; i++) {
if (/\s/.test(linestr[i])) {
spaces.push(linestr[i]);
} else {
break;
}
}
this.editor.edit((editBuilder) => {
editBuilder.insert(new vscode.Position(pos, 0), spaces.join("") + this.comment + "\n");
});
}
5.运行vscode插件
将光标定位在指定的函数或变量上,然后按快捷键ALt+/或右击菜单选择Add Comment即可添加jsDoc注释 

6.打包成vscode插件并发布
安装vscode插件打包工具
sh
yarn add -D @vscode/vsce
package.json
- icon:配置logo
- extensionKind:插件类型
workspace工作台功能或ui打开新的web页面,这里添加注释的功能是workspace - main:入口文件
json
{
"icon": "xcommentlogo.jpg",
"extensionKind": [
"workspace"
],
"main": "./dist/extension.js",
}
注意README上图片文件不可打包在其中,建议放到github上
执行打包命令,打包vsix插件包
sh
npx vsce package
登录Azure DevOps创建个人访问令牌,记得复制令牌token字符串


package.json里配置Azure DevOps发布者账号名
json
{
"publisher": "username",
}
执行命令登录账户,然后粘贴刚才复制的token字符串
sh
npx vsce login <username>

登录成功后执行发布命令
sh
npx vsce publish
也可以到vscode插件管理页面手动发布 https://marketplace.visualstudio.com/manage

注意:
- publisher注册和发布时,要使用谷歌的验证码recaptcha,可能要科学上网才能成功
- 发布插件的时候不要开启 fastGithub等代理,否则会验证失败
- 另外,一些临时文件不需要打包到vsix插件包的文件请在
.vscodeignore里面设置为忽略
vscode-xcomment这个注释小功能插件已发布到vscode插件市场,欢迎使用~
marketplace.visualstudio.com/items?itemN...

7.github地址
https://github.com/xiaolidan00/vscode-xcomment
参考
- vscode插件开发官方示例
https://github.com/microsoft/vscode-extension-samples - vscode插件开发教程
https://vscode.js.cn/api/get-started/your-first-extension - 打包发布vscode插件
https://vscode.js.cn/api/working-with-extensions/publishing-extension - Github Copilot
- astexplorer.net/