普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月2日首页

写个添加注释的vscode插件

2026年3月2日 10:59

写注释真的好烦,每次都得/**……*/的形式才有jsDoc的效果,真的不想浪费时间了,于是写个vscode插件,添加一下jsDoc注释,提升点效率

1.vscode插件开发脚手架

使用Yeoman脚手架工具和generator-codeVS Code 扩展生成器来生成一个vscode插件开发项目

# 安装
npm install -g yo generator-code
# 执行脚手架,生产项目
yo code

image.png

可以看到有不同的类型

  • 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会因为文件找不到而失败。

image.png

package.json配置命令,可以通过Ctrl+Shift+P唤起vscode命令栏搜索命令名称Hello World

 "contributes": {
    "commands": [
      {
        "command": "vscode-xcomment.helloWorld",
        "title": "Hello World"
      }
    ]
  },

src/extension.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

image.png

image.png

image.png

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

image.pngimage.png

image.png

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

image.png

3.添加快捷键和右击菜单

在package.json添加contributes.keybindings配置快捷键

 "contributes": {
  "keybindings": [
      {
        "command": "vscode-hello.helloWorld",
        "key": "alt+H"
      }
    ]
 }

在package.json添加contributes.menus.editor/context配置编辑器中右击菜单

  "contributes": {
   "editor/context": [
        {
          "command": "vscode-hello.helloWorld",          
          "group": "1_modification@100",
          "alt": "vscode-hello.helloWorld",
          "key": "alt+H"
        }
      ]
  }
  

image.png

可以直接通过右击菜单的Hello World或者Alt+H快捷键触发helloWorld命令

如果想将菜单放在别的地方或者别的分组group,可以查看官方文档contributes.menus的配置 vscode.js.cn/api/referen…

image.png

当然可以添加一些快捷键和菜单生效的条件设置,比如当前打开的代码是ts/js/vue文件才出现或生效

 "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获取当前打开文档的代码

   const editor = vscode.window.activeTextEditor;
  const doc = editor.document;
  const fileName = doc.fileName;//文件绝对路径
  const code = doc.getText();//代码内容

vscode检查是否有语法错误

执行命令前先进行语法错误判断,如果没有错误再执行

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判断文件类型

限定执行命令的文件类型

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;
}

注册命令并提示信息

    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当前光标所在位置

editor.selection.active

editor.selection.active.line//光标所在行
editor.selection.active.character//光标所在该行的第几个字符的位置

由于vscode按行来记录光标位置,所以为了方便找到具体字符位置,将代码按行进行分割,并进行索引开始结束位置和内容记录

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聚焦在该代码编辑上了,有时候打开文档但是没有聚焦光标
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代码
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

import * as ts from "typescript";
const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);

node打印出来可能很难清楚其结构,可以到astexplorer查看具体的AST树

image.png

遍历代码根节点下所有节点,查看节点类型和内容

sourceFile.statements.forEach((node) => {
//查看节点类型
  console.log(ts.SyntaxKind[node.kind]);
  console.log(node.getText());
  console.log("------");
});

image.png

查找光标位置的节点

遍历AST根据节点范围判断光标是否在该节点,然后深度遍历该节点,直到找到最终的子节点,即光标所在具体位置,期间可以收集所有父子节点。

因为每类节点的结构都有所差异,推荐使用ts自带的ts.forEachChild遍历子节点的方法

 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;
  }

获取当前光标所在的节点

 const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS); 
      const currentNodes = this.findNode(sourceFile, p); 

给不同节点添加注释

  • 判断该节点是否已有jsDoc注释,如果有则不添加注释。
  • 如果是单行注释或者非jsDoc的多行注释则转化为jsDoc注释
  • 如果没有注释则直接添加
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();
      }
    }
  }

如果已有注释则延用,如果没有注释则获取节点的名称作为注释内容

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;
  }

普通函数与方法

普通函数声明

//对应节点类型 FunctionDeclaration
function sum(a: number, b: number): number {
  return a + b;
}

方法定义

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函数定义

interface Shape {
//对应节点类型 MethodSignature
  draw(x: number, y: number): void;
}
type DrawType = {
//对应节点类型 MethodSignature
  draw(x: number, y: number): void;
};

符合此函数结构就添加注释

if (
  ts.isFunctionDeclaration(stmt) ||
  ts.isMethodDeclaration(stmt) ||
  ts.isMethodSignature(stmt) ||
  ts.isConstructorDeclaration(stmt)
) {
  this.checkDocs(stmt, sourceFile, this.addDocFun(stmt));
  return;
}

获取函数参数变量和返回类型的jsDoc注释

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;
  }

返回给普通函数添加注释的回调

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);
    };
  }

箭头函数与匿名函数

箭头函数

//对应的AST结构 VariableDeclaration.initializer:ArrowFunction
const myFun = (a: number, b: number): number => {
  return a + b;
};

给变量赋值匿名函数

//对应的AST结构 VariableDeclaration.initializer:FunctionExpression
const myFun = function (a: number, b: number): number {
  return a + b;
};

符合此结构就添加注释

if (
  ts.isVariableDeclaration(declaration) &&
  declaration.initializer &&
  (ts.isFunctionExpression(declaration.initializer) || ts.isArrowFunction(declaration.initializer))
) {
  this.checkDocs(declaration, sourceFile, this.addInitializerDoc(stmt, declaration.initializer));
  return;
}

返回给箭头函数和匿名函数添加注释的回调

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);
    };
  }

同理对象或类属性的箭头函数与匿名函数赋值

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);
  };
}
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;
}

给属性等添加注释

//对应节点类型 TypeAliasDeclaration
type AAA={
//对应节点类型 PropertySignature
aaa:string;
}

//对应节点类型 InterfaceDeclaration
interface BBB {
//对应节点类型 PropertySignature
bbb:string;
}
//对应节点类型 ClassDeclaration
class CCC{
//对应节点类型 PropertyDeclaration
ccc:string='hello';
}

符合该节点类型的添加注释

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;
}

返回属性等节点注释回调

 addDocProp(prop: ts.Node) {
    return (msg?: string[]) => {
      const comments = this.getNodeName(prop, msg);

      this.addNodeComment(prop, comments);
    };
  }

变量等于函数运行结果

//对应的AST结构 VariableDeclaration.initializer=CallExpression
const state = reactive({
  aaa: 1
});
//对应的AST结构 VariableDeclaration.initializer=CallExpression
const valRef = ref("hello");

符合结构添加注释

if (ts.isVariableDeclaration(declaration) && declaration.initializer && ts.isCallExpression(declaration.initializer)) {
  this.checkDocs(stmt, sourceFile, this.addDocProp(stmt));
  return;
}

其他节点添加注释

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库插入注释

给节点插入头部注释

addNodeComment(node: ts.Node, comments: string[]) {
    ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, "*" + comments.join("\n"), true);
  }

获取新的代码打印内容

 printCode(sourceFile: ts.SourceFile) {
    const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
    const printed = printer.printFile(sourceFile);
    return printed;
  }

替换ts/js来添加注释

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);
}

替换全文

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文本插入

记录需要插入的注释内容,因为只有一处添加注释,然后就会停止遍历光标所在位置的父子节点

addNodeComment(node: ts.Node, comments: string[]) {
    const c = "/**" + comments.join("\n") + "*/";
    this.comment = c;
  }

插入文本,获取光标所在行的文本,判断是否为空白字符,如果全是空白字符则直接插入,否则按照当行前面空格位置插入

//该行内容
    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注释 preview.gif

rightmenu.png

6.打包成vscode插件并发布

安装vscode插件打包工具

yarn add -D @vscode/vsce

package.json

  • icon:配置logo
  • extensionKind:插件类型workspace工作台功能或ui打开新的web页面,这里添加注释的功能是workspace
  • main:入口文件
{
 "icon": "xcommentlogo.jpg",
 "extensionKind": [
    "workspace"
  ],
  "main": "./dist/extension.js",
}

注意README上图片文件不可打包在其中,建议放到github上

执行打包命令,打包vsix插件包

npx vsce package

登录Azure DevOps创建个人访问令牌,记得复制令牌token字符串

image.png

image.png

package.json里配置Azure DevOps发布者账号名

{
"publisher": "username",
}

执行命令登录账户,然后粘贴刚才复制的token字符串

npx vsce login <username>

image.png

登录成功后执行发布命令

npx vsce publish

也可以到vscode插件管理页面手动发布 https://marketplace.visualstudio.com/manage

image.png

注意:

  • publisher注册和发布时,要使用谷歌的验证码recaptcha,可能要科学上网才能成功
  • 发布插件的时候不要开启 fastGithub等代理,否则会验证失败
  • 另外,一些临时文件不需要打包到vsix插件包的文件请在.vscodeignore 里面设置为忽略

vscode-xcomment这个注释小功能插件已发布到vscode插件市场,欢迎使用~

marketplace.visualstudio.com/items?itemN…

image.png

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/
❌
❌