CodeAction 与重构
灯泡图标是 VS Code 最聪明的交互之一:光标悬停在错误上,按下 Ctrl+. 就能一键修复或重构。这背后的机制就是 CodeAction——由插件按需提供、由编辑器统一展示的操作菜单。它把「发现问题的诊断」与「解决问题的动作」无缝衔接。
CodeAction 基础模型
一个 CodeAction 描述「一个可执行的动作」:一个标题、一个类型(kind)、一个可选的编辑内容或命令。
const action = new vscode.CodeAction('将数字改为常量', vscode.CodeActionKind.RefactorExtract);
// 三选一的执行方式:
action.edit = new vscode.WorkspaceEdit(); // 方式一:应用文本编辑
action.command = { command: 'myExt.doSomething', title: '执行' }; // 方式二:执行命令
// 方式三:command + arguments,让命令自己修改文档执行方式说明:
| 方式 | 适用 |
|---|---|
edit | 纯文本改动(插入/替换/删除) |
command | 需要额外逻辑(计算、弹窗、跨文件) |
edit + command | 先改文本再执行命令 |
实现 CodeActionProvider
provideCodeActions 返回操作数组:
import * as vscode from 'vscode';
class MyCodeActionProvider implements vscode.CodeActionProvider {
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): vscode.CodeAction[] {
const actions: vscode.CodeAction[] = [];
// 场景一:根据诊断生成快速修复
for (const diagnostic of context.diagnostics) {
const fix = this.fixForDiagnostic(document, diagnostic);
if (fix) {
actions.push(fix);
}
}
// 场景二:根据光标位置生成重构
const refactor = this.refactorAtRange(document, range);
if (refactor) {
actions.push(refactor);
}
return actions;
}
}
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
'plaintext',
new MyCodeActionProvider(),
{
providedCodeActionKinds: [
vscode.CodeActionKind.QuickFix,
vscode.CodeActionKind.Refactor
]
}
)
);
}触发时机
| 场景 | 说明 |
|---|---|
| 诊断出现 | context.diagnostics 携带该范围的诊断 |
| 光标移动/选中 | 编辑器在合适时机调用 Provider |
| 用户显式调用 | 打开 Quick Fix 菜单 |
注意:provideCodeActions 是同步返回的(类型为 CodeAction[]),不要在里面做耗时计算;需要异步时改用 CodeActionProvider<CodeAction> 泛型接口配合 provideCodeActions 返回 ProviderResult。
CodeActionKind 类型体系
Kind 是带层级的前缀字符串,编辑器按它分组与过滤操作:
quickfix
refactor
refactor.extract
refactor.inline
refactor.rewrite
source
source.organizeImports
source.fixAll层级关系与用法
| Kind 常量 | 完整值 | 用途 |
|---|---|---|
CodeActionKind.QuickFix | quickfix | 修复诊断问题 |
CodeActionKind.Refactor | refactor | 重构大类(提取、内联等) |
CodeActionKind.RefactorExtract | refactor.extract | 提取方法/常量/变量 |
CodeActionKind.RefactorInline | refactor.inline | 内联变量/函数 |
CodeActionKind.RefactorRewrite | refactor.rewrite | 重写代码块 |
CodeActionKind.Source | source | 源码级操作 |
CodeActionKind.SourceOrganizeImports | source.organizeImports | 整理导入 |
CodeActionKind.SourceFixAll | source.fixAll | 全部自动修复 |
层级匹配规则
Kind 支持 contains 与 append:
// contains:判断是否属于某个大类
vscode.CodeActionKind.RefactorExtract.contains('refactor.extract.constant');
// true——子层级属于父层级
// append:向下扩展自己的 kind
const myKind = vscode.CodeActionKind.Refactor.append('myOwn');
// 'refactor.myOwn'自定义重构建议以官方大类为前缀,例如提取常量用 RefactorExtract.append('constant'),这样能自动归入重构分组,也兼容 when 条件中的 kind 过滤。
disabled 原因与 isPreferred
显示置灰操作
某些操作在当前上下文不可用,但用户仍应看到它并明白为什么。用 CodeAction.disabled 置灰:
const action = new vscode.CodeAction('提取方法', vscode.CodeActionKind.RefactorExtract);
// 选中区域太小时无法提取
if (range.isEmpty) {
action.disabled = {
reason: '请先选中要提取的代码'
};
} else {
action.edit = buildExtractEdit(document, range);
}置灰后操作仍显示在菜单中,鼠标悬停展示 reason,点击无动作——比「不出现」更友好。
isPreferred 首选修复
多个同类型操作并列时,isPreferred: true 标记首选,按下快捷键(如 Ctrl+.)直接执行它而不再弹菜单:
const fixA = new vscode.CodeAction('删除多余空格', vscode.CodeActionKind.QuickFix);
fixA.edit = trimEdit;
fixA.isPreferred = true; // 首选
const fixB = new vscode.CodeAction('删除整行', vscode.CodeActionKind.QuickFix);
fixB.edit = deleteLineEdit;CodeActionProviderMetadata
Provider 元数据声明「本 Provider 会提供哪些 kind」,帮助编辑器做展示分组与预过滤:
class MyProvider implements vscode.CodeActionProvider {
// 声明提供哪些种类的操作
static readonly providedCodeActionKinds = [
vscode.CodeActionKind.QuickFix,
vscode.CodeActionKind.RefactorExtract,
vscode.CodeActionKind.SourceOrganizeImports
];
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range,
context: vscode.CodeActionContext
): vscode.CodeAction[] {
// 只生成声明过的 kind
const actions: vscode.CodeAction[] = [];
if (context.only) {
// context.only 是编辑器请求的 kind 过滤
if (context.only.contains(vscode.CodeActionKind.QuickFix)) {
actions.push(...this.quickFixes(document, context));
}
if (context.only.contains(vscode.CodeActionKind.RefactorExtract)) {
actions.push(this.extractAction(document, range));
}
return actions;
}
// 无过滤时返回全部
return [...this.quickFixes(document, context),
this.extractAction(document, range)];
}
}context.only 值得注意:编辑器可能只为某个菜单(如 Refactor 子菜单)调用 Provider,此时只生成对应 kind 能显著减少计算。
完整示例:提取常量与快速修复
做一个纯文本「常量提取器」:选中一段数字,一键提取为顶部常量;同时为「行尾分号缺失」提供快速修复。
// extension.ts
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const provider = new RefactorFixProvider();
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
'plaintext', provider, {
providedCodeActionKinds: [
vscode.CodeActionKind.QuickFix,
vscode.CodeActionKind.RefactorExtract
]
}
)
);
}
class RefactorFixProvider implements vscode.CodeActionProvider {
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range,
context: vscode.CodeActionContext
): vscode.CodeAction[] {
const actions: vscode.CodeAction[] = [];
// 1) 提取数字为常量(重构)
const extract = this.extractConstantAction(document, range);
if (extract) {
actions.push(extract);
}
// 2) 行尾补分号(快速修复)
const semicolon = this.addSemicolonFix(document, range);
if (semicolon) {
actions.push(semicolon);
}
return actions;
}
// 选中纯数字时提供「提取常量」
private extractConstantAction(
document: vscode.TextDocument,
range: vscode.Range
): vscode.CodeAction | undefined {
const text = document.getText(range);
// 只处理纯数字(支持小数)
if (!/^\d+(\.\d+)?$/.test(text)) {
return undefined;
}
const action = new vscode.CodeAction(
`提取常量 const MAGIC = ${text}`,
vscode.CodeActionKind.RefactorExtract.append('constant')
);
// 在文件顶部插入常量定义
const edit = new vscode.WorkspaceEdit();
const insertPos = new vscode.Position(0, 0);
const header = `const MAGIC = ${text};\n\n`;
edit.insert(document.uri, insertPos, header);
// 把选中处替换为常量引用
edit.replace(document.uri, range, 'MAGIC');
action.edit = edit;
action.isPreferred = false;
return action;
}
// 诊断/光标处缺分号时提供快速修复
private addSemicolonFix(
document: vscode.TextDocument,
range: vscode.Range
): vscode.CodeAction | undefined {
const line = range.start.line;
const text = document.lineAt(line).text;
// 空行或已有分号则跳过
if (!text.trim() || text.trimEnd().endsWith(';')) {
return undefined;
}
const action = new vscode.CodeAction(
'在行尾添加分号',
vscode.CodeActionKind.QuickFix
);
action.isPreferred = true;
const edit = new vscode.WorkspaceEdit();
const endPos = new vscode.Position(line, text.length);
edit.insert(document.uri, endPos, ';');
action.edit = edit;
return action;
}
}提取方法(进阶示例)
文本级的「提取方法」需要更强的文本分析,这里给出思路框架:选定多行文本,抽离为一个函数调用,函数体追加到文件末尾。
function extractMethodAction(
document: vscode.TextDocument,
range: vscode.Range
): vscode.CodeAction | undefined {
// 空选区无法提取
if (range.isEmpty) {
return undefined;
}
const bodyText = document.getText(range);
const methodName = `extractedFunc_${Date.now() % 10000}`;
// 生成方法定义文本
const methodDef = `\n\nfunction ${methodName}() {\n` +
bodyText.split('\n').map((l) => ` ${l}`).join('\n') +
`\n}`;
const edit = new vscode.WorkspaceEdit();
// 1) 原位置替换为调用
edit.replace(document.uri, range, `${methodName}();`);
// 2) 文件末尾追加方法定义
const lastLine = document.lineAt(document.lineCount - 1);
const docEnd = new vscode.Position(
document.lineCount - 1, lastLine.text.length
);
edit.insert(document.uri, docEnd, methodDef);
const action = new vscode.CodeAction(
`提取方法 ${methodName}`,
vscode.CodeActionKind.RefactorExtract
);
action.edit = edit;
return action;
}真实项目中的提取方法要处理变量作用域、返回值、参数推导,通常依赖语法树(如 TypeScript AST 或 Tree-sitter),文本切割只是最小可用实现。
与诊断联动
快速修复的标准链路:诊断消息里带 code,Provider 按 code 匹配生成修复:
// 诊断侧:设置可识别的错误码
const diagnostic = new vscode.Diagnostic(
range, '缺少分号', vscode.DiagnosticSeverity.Error
);
diagnostic.code = 'missing-semicolon';
// Provider 侧:按 code 匹配
for (const d of context.diagnostics) {
if (d.code === 'missing-semicolon') {
const action = new vscode.CodeAction(
'自动补分号', vscode.CodeActionKind.QuickFix
);
action.diagnostics = [d]; // 关联诊断,菜单中显示
action.edit = ...;
actions.push(action);
}
}action.diagnostics 把修复与诊断绑定,编辑器在问题面板中也能直接点出修复按钮。
常见问题
| 问题 | 处理 |
|---|---|
| 灯泡不出现 | 检查注册的语言范围与 providedCodeActionKinds |
| 操作在错误菜单分组 | kind 以官方大类为前缀 |
| 重构置灰但没原因 | 设置 disabled.reason |
| 首选修复不生效 | 确认 isPreferred 且只有一个首选 |
| context.only 过滤后无结果 | 按 only 过滤分支返回 |
| 编辑不生效 | 检查 WorkspaceEdit 的 uri 与 range 有效性 |
| 长操作卡界面 | 改异步提供或把耗时逻辑移入 command |
调试技巧
| 场景 | 手段 |
|---|---|
| 查看 Provider 调用 | 输出面板选择对应语言频道看日志 |
| 检查返回的操作 | 在 provideCodeActions 里 console.log |
| 验证 edit 内容 | 应用前打印 textEdits 数组 |
| 触发时机 | 打开 Quick Fix 菜单强制触发 |
CodeAction 是插件与编辑器「对话」的高层接口:诊断负责发现问题,CodeAction 负责解决问题。一个成熟的插件,往往是两者配合的产物。