从零写一个 VS Code 插件“中文字数统计”:在状态栏实时显示 Markdown 和纯文本文件的中文字数、英文单词数和预计阅读时间,选中文本时只统计选中部分,最后打包成 .vsix 文件安装或分享。
先看清成品
成品是一个 TypeScript 插件项目。打开 Markdown 文件时,状态栏右侧显示“📖 1234 字 · 4 分钟”,点击弹出详细统计;阅读速度可以在设置里修改。本文代码已实际编译,4 个单元测试全部通过,并用官方 vsce 4.0.0 打包出 4.82 KB 的 .vsix。在 VS Code 里实际点击的效果未在本文环境中截图验证。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
| 插件源码 | src/extension.ts + src/count.ts |
| 单元测试 | 用 Node 自带测试运行器,不依赖 VS Code |
| 调试配置 | F5 打开一个加载了插件的新窗口 |
| 安装包 | zh-word-count-0.1.0.vsix |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| VS Code | 1.100 或更高 | 插件声明的最低版本 |
| Node.js | 22 或更高 | 打包工具 vsce 4.x 要求 Node 22 |
| TypeScript | 5.x | 作为开发依赖安装 |
| vsce | @vscode/vsce |
官方打包和发布工具 |
第一步:项目结构是什么样的?
zh-word-count/
├── .vscode/
│ └── launch.json
├── src/
│ ├── extension.ts # 插件入口:状态栏、事件、命令
│ ├── count.ts # 纯函数:统计逻辑
│ └── count.test.ts # 单元测试
├── package.json # 插件清单
├── tsconfig.json
├── .vscodeignore # 打包时排除的文件
├── README.md
└── LICENSE
官方也提供脚手架 npx --package yo --package generator-code -- yo code,会生成更完整的模板。本文手写最小结构,方便看清每个文件的作用。
1. 让 AI 帮你写(可选)
帮我写一个 VS Code 插件,TypeScript:
- 只在 markdown 和 plaintext 文件激活
- 状态栏右侧显示汉字数 + 英文单词数和预计阅读分钟数
- 有选中内容时只统计选中部分
- Markdown 统计时排除代码块、行内代码、图片和链接地址
- 统计逻辑写成独立纯函数,便于用 node --test 测试
- 阅读速度做成可配置项 zhWordCount.charsPerMinute,默认 400
第二步:插件清单 package.json 怎么写?
{
"name": "zh-word-count",
"displayName": "中文字数统计",
"description": "在状态栏实时显示 Markdown 和纯文本的中文字数、英文单词数和预计阅读时间",
"version": "0.1.0",
"publisher": "your-name",
"license": "MIT",
"engines": { "vscode": "^1.100.0" },
"categories": ["Other"],
"activationEvents": ["onLanguage:markdown", "onLanguage:plaintext"],
"main": "./out/extension.js",
"contributes": {
"commands": [
{ "command": "zhWordCount.showDetail", "title": "字数统计:显示详情" }
],
"configuration": {
"title": "中文字数统计",
"properties": {
"zhWordCount.charsPerMinute": {
"type": "number",
"default": 400,
"description": "阅读速度(中文字/分钟),用于估算阅读时间"
}
}
}
},
"scripts": {
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"test": "npm run compile && node --test out/count.test.js"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/vscode": "^1.100.0",
"typescript": "^5.8.0"
}
}
| 字段 | 作用 |
|---|---|
engines.vscode |
最低支持的 VS Code 版本,要和 @types/vscode 版本对应 |
activationEvents |
打开 Markdown 或纯文本时才加载插件 |
main |
编译后的入口文件 |
contributes.commands |
注册到命令面板的命令 |
contributes.configuration |
出现在设置界面的配置项 |
第三步:核心代码怎么写?
1. 统计逻辑 src/count.ts
// 纯函数:不依赖 VS Code,方便用 node --test 单独测试
export interface CountResult {
cjk: number; // 中日韩汉字数
words: number; // 英文单词数(连续字母或数字算一个)
total: number; // 汉字 + 单词
minutes: number; // 预计阅读分钟数,至少 1
}
const CJK = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/g;
const WORD = /[A-Za-z0-9]+(?:['’-][A-Za-z0-9]+)*/g;
export function stripMarkdown(text: string): string {
return text
.replace(/```[\s\S]*?```/g, "") // 代码块不计入
.replace(/`[^`]*`/g, "") // 行内代码
.replace(/!\[[^\]]*\]\([^)]*\)/g, "") // 图片
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // 链接只保留文字
.replace(/^#{1,6}\s+/gm, ""); // 标题符号
}
export function count(text: string, charsPerMinute = 400, markdown = false): CountResult {
const body = markdown ? stripMarkdown(text) : text;
const cjk = (body.match(CJK) ?? []).length;
const words = (body.replace(CJK, " ").match(WORD) ?? []).length;
const total = cjk + words;
// 英文按每分钟 200 词折算成“汉字当量”
const minutes = Math.max(1, Math.ceil((cjk + words * (charsPerMinute / 200)) / charsPerMinute));
return { cjk, words, total, minutes };
}
把统计逻辑和 VS Code API 分开,是插件开发里很实用的习惯:纯函数可以在几毫秒内测试完,不需要启动编辑器。
2. 插件入口 src/extension.ts
import * as vscode from "vscode";
import { count } from "./count";
let item: vscode.StatusBarItem;
export function activate(context: vscode.ExtensionContext) {
item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
item.command = "zhWordCount.showDetail";
context.subscriptions.push(item);
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(update),
vscode.window.onDidChangeTextEditorSelection(update),
vscode.workspace.onDidChangeTextDocument((e) => {
if (e.document === vscode.window.activeTextEditor?.document) update();
}),
vscode.commands.registerCommand("zhWordCount.showDetail", showDetail)
);
update();
}
function currentText(editor: vscode.TextEditor): { text: string; selected: boolean } {
const sel = editor.selection;
if (!sel.isEmpty) return { text: editor.document.getText(sel), selected: true };
return { text: editor.document.getText(), selected: false };
}
function cpm(): number {
return vscode.workspace.getConfiguration("zhWordCount").get<number>("charsPerMinute", 400);
}
function update() {
const editor = vscode.window.activeTextEditor;
if (!editor || !["markdown", "plaintext"].includes(editor.document.languageId)) {
item.hide();
return;
}
const { text, selected } = currentText(editor);
const r = count(text, cpm(), editor.document.languageId === "markdown");
item.text = `$(book) ${selected ? "选中 " : ""}${r.total} 字 · ${r.minutes} 分钟`;
item.tooltip = "点击查看详细统计";
item.show();
}
function showDetail() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const { text, selected } = currentText(editor);
const r = count(text, cpm(), editor.document.languageId === "markdown");
vscode.window.showInformationMessage(
`${selected ? "选中内容" : "全文"}:汉字 ${r.cjk},英文单词 ${r.words},合计 ${r.total},预计阅读 ${r.minutes} 分钟`
);
}
export function deactivate() {}
| 代码 | 作用 |
|---|---|
createStatusBarItem |
创建状态栏条目 |
$(book) |
内置图标语法,显示一本书 |
onDidChangeTextDocument |
内容变化时刷新 |
onDidChangeTextEditorSelection |
选区变化时刷新 |
context.subscriptions.push |
插件停用时自动清理 |
3. 测试 src/count.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { count, stripMarkdown } from "./count";
test("纯中文按字计数", () => {
assert.equal(count("你好,世界").cjk, 4);
});
test("中英混排分别计数", () => {
const r = count("我在用 VS Code 写 TypeScript 插件");
assert.equal(r.cjk, 6);
assert.equal(r.words, 3);
assert.equal(r.total, 9);
});
test("Markdown 去掉代码块和链接地址", () => {
const md = "# 标题\n正文[链接](https://example.com)\n```js\nconst a = 1;\n```";
assert.equal(stripMarkdown(md).includes("const"), false);
assert.equal(count(md, 400, true).cjk, 6);
assert.equal(count(md, 400, true).words, 0);
});
test("阅读时间至少 1 分钟,按速度向上取整", () => {
assert.equal(count("").minutes, 1);
assert.equal(count("字".repeat(801), 400).minutes, 3);
});
4. tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"outDir": "out",
"rootDir": "src",
"lib": ["ES2022"],
"sourceMap": true,
"strict": true,
"esModuleInterop": true
}
}
第四步:怎样编译、测试和调试?
1. 安装依赖并测试
npm install
npm test
本文实际输出(节选):
ok 1 - 纯中文按字计数
ok 2 - 中英混排分别计数
ok 3 - Markdown 去掉代码块和链接地址
ok 4 - 阅读时间至少 1 分钟,按速度向上取整
# pass 4
# fail 0
2. 调试配置 .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "运行插件",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile"
}
]
}
npm: compile 是 VS Code 根据 package.json 里的 scripts 自动识别出的任务,不需要额外写 tasks.json。
3. 按 F5 启动
VS Code 会编译代码并打开一个标题带 [Extension Development Host] 的新窗口。
4. 检查结果
| 操作 | 期望 |
|---|---|
| 新窗口里打开一个 .md 文件 | 状态栏右侧出现字数 |
| 输入文字 | 数字实时变化 |
| 选中一段 | 显示“选中 N 字” |
| 点击状态栏 | 弹出详细统计 |
| 打开 .js 文件 | 状态栏条目隐藏 |
| 设置里搜 zhWordCount | 能修改阅读速度 |
第五步:怎样打包和安装?
1. 打包
npx @vscode/vsce package
.vscodeignore 排除了源码、测试和调试配置,安装包只保留运行需要的文件:
src/**
out/**/*.test.js
out/**/*.map
tsconfig.json
.vscode/**
本文实际打包结果:
DONE Packaged: zh-word-count-0.1.0.vsix (8 files, 4.82 KB)
如果 package.json 没写 repository 字段,vsce 会询问是否继续,可以加 --allow-missing-repository 跳过。
2. 安装
code --install-extension zh-word-count-0.1.0.vsix
也可以在扩展面板右上角的“…”菜单里选择“从 VSIX 安装”。
3. 发布到扩展市场(可选)
需要先在 Visual Studio Marketplace 创建发布者,并把 publisher 改成你的发布者 ID,再执行 npx @vscode/vsce publish。详细步骤见项目资源里的发布文档。
出问题时该怎么调?
1. 编译报 esModuleInterop 错误
本文第一次编译时遇到:
src/count.test.ts(2,8): error TS1259: Module '"node:assert/strict"' can only be default-imported using the 'esModuleInterop' flag
在 tsconfig.json 的 compilerOptions 里加上 "esModuleInterop": true 即可。
2. 按 F5 后状态栏没有显示
依次检查:
| 检查 | 方法 |
|---|---|
| 插件是否激活 | 新窗口里打开的必须是 markdown 或纯文本文件 |
| 是否报错 | 原窗口的“调试控制台”查看异常 |
| 编译产物 | out/extension.js 是否存在 |
3. 汉字数和其他工具不一样
不同工具对标点、数字、英文的算法不同。本插件不计标点,英文按单词计,数字串算一个单词。需要和平台规则一致时,修改 count.ts 里的正则并补充测试。
4. vsce 报 Node 版本错误
vsce 4.x 要求 Node 22 或更高,用 node -v 检查后升级。
常见问题怎么处理?
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 安装时提示版本不兼容 | engines.vscode 高于本机 VS Code |
升级 VS Code 或调低版本要求 |
@types/vscode 版本冲突 |
类型版本高于 engines.vscode |
两者保持一致 |
| 命令面板找不到命令 | contributes.commands 拼写错 |
命令 ID 和 registerCommand 一致 |
| 修改代码不生效 | 没重新编译 | npm run watch 后在调试窗口 Ctrl+R 重载 |
| 打包提示缺少 LICENSE | 没有许可证文件 | 添加 LICENSE |
| 大文件输入卡顿 | 每次按键都全文统计 | 可加防抖,延迟 300 毫秒再统计 |
项目资源
建议先只做到第四步:跑通单元测试,再按 F5 看到状态栏出现字数。
之后再按自己的需求改统计规则,比如加上“不含空格的字符数”,每改一次补一条测试。