CodeLens 代码透镜
CodeLens 是在代码行上方显示的「内联命令」:显示引用计数、一键运行测试、查看实现。它把操作入口直接放在相关代码旁,上下文关联极强。
注册 CodeLens Provider
languages.registerCodeLensProvider 注册透镜提供器:
typescript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
'plaintext',
{
provideCodeLenses(
document: vscode.TextDocument,
token: vscode.CancellationToken
): vscode.CodeLens[] {
// 返回 CodeLens 数组
return [
new vscode.CodeLens(
new vscode.Range(0, 0, 0, 10), // 显示位置
{
command: 'myExt.runCode',
title: '运行代码',
arguments: [document]
}
)
];
}
}
)
);
}CodeLens 属性
typescript
const lens = new vscode.CodeLens(
range, // 显示范围(代码行上方)
command // 点击执行的命令
);
// 可选:是否显示(动态控制)
lens.isResolved = false;| 属性 | 作用 |
|---|---|
range | 透镜显示位置 |
command | 点击执行的命令 |
isResolved | 是否已解析 |
Command 配置
CodeLens 的命令包含完整交互信息:
typescript
const lens = new vscode.CodeLens(range, {
command: 'myExt.runTest',
title: '运行测试', // 显示文本
tooltip: '点击运行此测试', // 悬停提示
arguments: [document.uri, lineNumber] // 参数
});onDidChangeCodeLenses 动态刷新
透镜内容变化时,通过事件通知刷新:
typescript
class LensProvider implements vscode.CodeLensProvider {
// 变更事件:调用 fire 触发重新提供
private _onDidChangeCodeLenses =
new vscode.EventEmitter<void>();
readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event;
provideCodeLenses(document): vscode.CodeLens[] {
// 根据最新数据生成透镜
return this.buildLenses(document);
}
// 数据变化时触发刷新
refresh() {
this._onDidChangeCodeLenses.fire();
}
}刷新时机
| 场景 | 触发 |
|---|---|
| 文档内容变化 | 文档变更事件 |
| 引用数量变化 | 数据源更新 |
| 测试状态变化 | 运行完成回调 |
动态刷新示例:引用计数
typescript
import * as vscode from 'vscode';
class RefCountLensProvider implements vscode.CodeLensProvider {
private _onDidChangeCodeLenses =
new vscode.EventEmitter<void>();
readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event;
provideCodeLenses(document): vscode.CodeLens[] {
const lenses: vscode.CodeLens[] = [];
// 为每个函数定义添加引用计数透镜
for (let line = 0; line < document.lineCount; line++) {
const text = document.lineAt(line).text;
const match = text.match(/function\s+(\w+)/);
if (match) {
const name = match[1];
const count = this.countReferences(document, name);
lenses.push(
new vscode.CodeLens(
new vscode.Range(line, 0, line, text.length),
{
command: 'myExt.showRefs',
title: `${count} 处引用`,
arguments: [document.uri, name]
}
)
);
}
}
return lenses;
}
// 统计引用数量
private countReferences(
document: vscode.TextDocument,
name: string
): number {
const text = document.getText();
const regex = new RegExp(`\\b${name}\\b`, 'g');
return (text.match(regex) || []).length;
}
refresh() {
this._onDidChangeCodeLenses.fire();
}
}
export function activate(context: vscode.ExtensionContext) {
const provider = new RefCountLensProvider();
context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
'plaintext',
provider
)
);
// 文档变化时刷新透镜
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
if (event.document.languageId === 'plaintext') {
provider.refresh();
}
})
);
}运行测试场景
测试类插件最常见的 CodeLens 用法:
typescript
import * as vscode from 'vscode';
class TestLensProvider implements vscode.CodeLensProvider {
private testStates = new Map<string, string>(); // 测试状态缓存
provideCodeLenses(document): vscode.CodeLens[] {
const lenses: vscode.CodeLens[] = [];
const text = document.getText();
// 匹配测试函数
const testRegex = /(?:it|test)\(['"]([^'"]+)['"]/g;
let match: RegExpExecArray | null;
while ((match = testRegex.exec(text))) {
const name = match[1];
const position = document.positionAt(match.index);
const line = position.line;
const lineText = document.lineAt(line).text;
const state = this.testStates.get(name);
const stateTitle = state
? `$(circle-filled) ${state}`
: '运行测试';
// 运行按钮
lenses.push(
new vscode.CodeLens(
new vscode.Range(line, 0, line, lineText.length),
{
command: 'myExt.runTest',
title: stateTitle,
arguments: [name]
}
)
);
// 调试按钮
lenses.push(
new vscode.CodeLens(
new vscode.Range(line, 0, line, lineText.length),
{
command: 'myExt.debugTest',
title: '$(debug) 调试',
arguments: [name]
}
)
);
}
return lenses;
}
// 测试完成后更新状态
updateTestState(name: string, state: string) {
this.testStates.set(name, state);
this._onDidChangeCodeLenses.fire();
}
}完整示例:功能开关透镜
typescript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const provider = new FeatureToggleLensProvider();
context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
'plaintext',
provider
)
);
// 切换功能开关命令
context.subscriptions.push(
vscode.commands.registerCommand(
'myExt.toggleFeature',
(document: vscode.TextDocument, line: number) => {
const editor = vscode.window.visibleTextEditors
.find((e) => e.document.uri.toString() === document.uri.toString());
if (!editor) {
return;
}
const range = new vscode.Range(line, 0, line, 1000);
const current = editor.document.getText(range);
const next = current.includes('true')
? current.replace('true', 'false')
: current.replace('false', 'true');
editor.edit((builder) => builder.replace(range, next));
provider.refresh();
}
)
);
}
class FeatureToggleLensProvider implements vscode.CodeLensProvider {
private _onDidChangeCodeLenses =
new vscode.EventEmitter<void>();
readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event;
provideCodeLenses(document): vscode.CodeLens[] {
const lenses: vscode.CodeLens[] = [];
for (let line = 0; line < document.lineCount; line++) {
const text = document.lineAt(line).text;
// 匹配功能开关行
const match = text.match(/feature:\s*(true|false)/);
if (match) {
const state = match[1] === 'true' ? '已启用' : '已禁用';
lenses.push(
new vscode.CodeLens(
new vscode.Range(line, 0, line, text.length),
{
command: 'myExt.toggleFeature',
title: `$(sync) ${state},点击切换`,
arguments: [document, line]
}
)
);
}
}
return lenses;
}
refresh() {
this._onDidChangeCodeLenses.fire();
}
}显示优化
| 场景 | 处理 |
|---|---|
| 透镜过多 | 只对关键行添加 |
| 内容过期 | onDidChangeCodeLenses 刷新 |
| 图标 | title 中支持 $(图标) |
| 点击无效 | 确认命令已注册 |
常见问题
| 问题 | 处理 |
|---|---|
| 透镜不显示 | 检查 range 与语言注册 |
| 点击无反应 | 确认 command 已注册 |
| 内容不更新 | 触发 onDidChangeCodeLenses |
| 位置错乱 | 精确定位 range |
CodeLens 把「命令入口」嵌入代码上下文,是提升代码可操作性的高价值特性。