LSP 开发指南 / 第 8 章:代码动作
8.1 代码动作概述
代码动作(Code Actions)是 LSP 中最灵活的特性之一,它将诊断修复、代码重构、代码透镜等统一到一个接口下。
8.1.1 代码动作类型
| 类型 | CodeActionKind | 触发方式 | 典型用途 |
|---|---|---|---|
| Quick Fix | quickfix | 点击灯泡图标 / Ctrl+. | 修复诊断错误 |
| Refactor | refactor | Ctrl+Shift+R | 重构操作 |
| Source | source | 右键菜单 | 全文件操作 |
| Code Lens | codeLens | 点击行内提示 | 附加信息操作 |
8.2 Code Action 请求
8.2.1 请求与响应
Client 请求:
{
"jsonrpc": "2.0",
"id": 30,
"method": "textDocument/codeAction",
"params": {
"textDocument": { "uri": "file:///src/main.py" },
"range": {
"start": { "line": 10, "character": 4 },
"end": { "line": 10, "character": 15 }
},
"context": {
"diagnostics": [
{
"range": {
"start": { "line": 10, "character": 4 },
"end": { "line": 10, "character": 15 }
},
"severity": 1,
"source": "my-lsp",
"message": "Undefined variable 'old_name'",
"code": "undefined-variable"
}
],
"only": ["quickfix"]
}
}
}
Server 响应:
{
"jsonrpc": "2.0",
"id": 30,
"result": [
{
"title": "Rename to 'new_name'",
"kind": "quickfix",
"diagnostics": [
{
"range": { "start": { "line": 10, "character": 4 }, "end": { "line": 10, "character": 15 } },
"severity": 1,
"message": "Undefined variable 'old_name'"
}
],
"edit": {
"changes": {
"file:///src/main.py": [
{
"range": {
"start": { "line": 10, "character": 4 },
"end": { "line": 10, "character": 15 }
},
"newText": "new_name"
}
]
}
}
},
{
"title": "Add import for 'old_name'",
"kind": "quickfix",
"diagnostics": [],
"command": {
"title": "Add import",
"command": "myLsp.addImport",
"arguments": ["old_name", "file:///src/main.py"]
}
}
]
}
8.2.2 CodeAction vs Command
| 方式 | 说明 | 适用场景 |
|---|---|---|
| CodeAction + edit | 直接返回文本编辑 | 简单的文本替换 |
| CodeAction + command | 返回命令,由 Server 或 Client 执行 | 复杂操作(需额外逻辑) |
| CodeAction + edit + command | 先应用编辑,再执行命令 | 需要编辑和额外操作 |
8.3 Quick Fix 实现
connection.onRequest("textDocument/codeAction", (params) => {
const doc = docManager.get(params.textDocument.uri);
if (!doc) return [];
const actions: CodeAction[] = [];
// 根据诊断生成 Quick Fix
for (const diagnostic of params.context.diagnostics) {
if (diagnostic.code === "undefined-variable") {
const match = diagnostic.message.match(/Undefined variable '(\w+)'/);
if (match) {
const varName = match[1];
// Quick Fix 1:自动导入
actions.push({
title: `Import '${varName}' from available modules`,
kind: "quickfix",
diagnostics: [diagnostic],
edit: {
changes: {
[params.textDocument.uri]: [
{
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
newText: `import ${varName}\n`,
},
],
},
},
});
// Quick Fix 2:拼写建议
const suggestions = findSimilarNames(doc, varName);
for (const suggestion of suggestions) {
actions.push({
title: `Rename to '${suggestion}'`,
kind: "quickfix",
diagnostics: [diagnostic],
edit: {
changes: {
[params.textDocument.uri]: [
{
range: diagnostic.range,
newText: suggestion,
},
],
},
},
});
}
}
}
}
return actions;
});
8.4 重构动作
8.4.1 常用重构类型
| Kind | 说明 | 快捷键 (VS Code) |
|---|---|---|
refactor.extract | 提取(函数/变量/常量) | Ctrl+Shift+R → E |
refactor.inline | 内联 | Ctrl+Shift+R → I |
refactor.rewrite | 重写 | Ctrl+Shift+R → R |
refactor.move | 移动 | — |
8.4.2 提取函数重构
function provideExtractFunctionAction(
doc: TextDocumentItem,
range: Range
): CodeAction | null {
const selectedText = getTextInRange(doc.text, range);
if (!selectedText || selectedText.trim().length === 0) return null;
// 检查选区是否适合提取
if (!isExtractable(selectedText)) return null;
const funcName = "extracted_function";
const indent = getIndentAtLine(doc.text, range.start.line);
const params = findFreeVariables(doc.text, range);
const funcDef =
`${indent}def ${funcName}(${params.join(", ")}):\n` +
selectedText
.split("\n")
.map((line) => " " + line)
.join("\n") +
"\n\n";
const funcCall = `${funcName}(${params.join(", ")})`;
return {
title: "Extract to function",
kind: "refactor.extract",
edit: {
changes: {
[doc.uri]: [
// 在选区前插入函数定义
{
range: { start: range.start, end: range.start },
newText: funcDef,
},
// 替换选区为函数调用
{
range: range,
newText: funcCall,
},
],
},
},
};
}
8.4.3 提取变量重构
function provideExtractVariableAction(
doc: TextDocumentItem,
range: Range
): CodeAction | null {
const selectedText = getTextInRange(doc.text, range);
if (!selectedText || !isExpression(selectedText)) return null;
const varName = "extractedVar";
const indent = getIndentAtLine(doc.text, range.start.line);
const edits: TextEdit[] = [
// 在当前行前插入变量声明
{
range: { start: { line: range.start.line, character: 0 }, end: { line: range.start.line, character: 0 } },
newText: `${indent}${varName} = ${selectedText}\n`,
},
// 替换表达式为变量引用
{
range: range,
newText: varName,
},
];
return {
title: `Extract to variable '${varName}'`,
kind: "refactor.extract",
edit: {
changes: { [doc.uri]: edits },
},
};
}
8.4.4 重构动作的 CodeActionKind 层级
refactor
├── refactor.extract
│ ├── refactor.extract.function
│ ├── refactor.extract.variable
│ └── refactor.extract.constant
├── refactor.inline
│ ├── refactor.inline.function
│ └── refactor.inline.variable
├── refactor.rewrite
│ ├── refactor.rewrite.arrow
│ └── refactor.rewrite.import
└── refactor.move
└── refactor.move.file
8.5 Code Lens
Code Lens 在代码行内嵌入可点击的提示信息。
8.5.1 工作流程
┌───────────────┐ ┌───────────────┐
│ Client │ │ Server │
└───────┬───────┘ └───────┬───────┘
│ │
│ textDocument/codeLens │
│──────────────────────▶│
│ │
│ CodeLens[] │
│◀──────────────────────│
│ │ (显示 "3 references" 等提示)
│ │
│ codeLens/resolve │
│──────────────────────▶│ (用户悬停/点击时解析)
│ │
│ CodeLens (complete)│
│◀──────────────────────│
│ │
│ (用户点击 CodeLens) │
│ │
│ 执行 command │
8.5.2 实现示例
// Server 声明 Code Lens 支持
capabilities: {
codeLensProvider: {
resolveProvider: true,
},
}
// 提供 Code Lens
connection.onRequest("textDocument/codeLens", (params) => {
const doc = docManager.get(params.textDocument.uri);
if (!doc) return [];
const lenses: CodeLens[] = [];
const lines = doc.text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 类定义:显示 "X references" 和 "X implementations"
const classMatch = line.match(/^class\s+(\w+)/);
if (classMatch) {
const className = classMatch[1];
lenses.push({
range: {
start: { line: i, character: line.indexOf(className) },
end: { line: i, character: line.indexOf(className) + className.length },
},
command: {
title: "references", // 初始显示,resolve 后更新
command: "",
},
data: { type: "references", name: className, uri: params.textDocument.uri },
});
lenses.push({
range: {
start: { line: i, character: line.indexOf(className) },
end: { line: i, character: line.indexOf(className) + className.length },
},
command: {
title: "implementations",
command: "",
},
data: { type: "implementations", name: className, uri: params.textDocument.uri },
});
}
// 函数定义:显示引用数
const funcMatch = line.match(/^(?:def|function)\s+(\w+)/);
if (funcMatch) {
lenses.push({
range: {
start: { line: i, character: line.indexOf(funcMatch[1]) },
end: { line: i, character: line.indexOf(funcMatch[1]) + funcMatch[1].length },
},
command: { title: "references", command: "" },
data: { type: "references", name: funcMatch[1], uri: params.textDocument.uri },
});
}
}
return lenses;
});
// 懒加载解析
connection.onRequest("codeLens/resolve", async (codeLens) => {
const { type, name, uri } = codeLens.data;
if (type === "references") {
const refs = await findAllReferences(name, uri);
codeLens.command = {
title: `${refs.length} reference${refs.length !== 1 ? "s" : ""}`,
command: "editor.action.showReferences",
arguments: [uri, codeLens.range.start, refs],
};
} else if (type === "implementations") {
const impls = await findAllImplementations(name, uri);
codeLens.command = {
title: `${impls.length} implementation${impls.length !== 1 ? "s" : ""}`,
command: "editor.action.showReferences",
arguments: [uri, codeLens.range.start, impls],
};
}
return codeLens;
});
8.5.3 Code Lens 场景
| 场景 | 显示内容 | 命令 |
|---|---|---|
| 引用计数 | “3 references” | 跳转到引用列表 |
| 实现计数 | “2 implementations” | 跳转到实现列表 |
| 测试状态 | “✅ passed” / “❌ failed” | 运行测试 |
| Git 信息 | “last modified: 2 days ago” | 打开 git blame |
| 类型注解 | inferred type | 添加类型注解 |
8.6 Source Actions
Source Actions 是对整个文件的操作:
connection.onRequest("textDocument/codeAction", (params) => {
const actions: CodeAction[] = [];
// 整理导入
actions.push({
title: "Organize imports",
kind: "source.organizeImports",
edit: organizeImports(docManager.get(params.textDocument.uri)),
});
// 删除未使用的导入
actions.push({
title: "Remove unused imports",
kind: "source.removeUnusedImports",
edit: removeUnusedImports(docManager.get(params.textDocument.uri)),
});
// 添加所有缺失的导入
actions.push({
title: "Add all missing imports",
kind: "source.addMissingImports",
edit: addMissingImports(docManager.get(params.textDocument.uri)),
});
return actions;
});
8.7 Code Action 的 resolve 模式
对于计算成本较高的代码动作,可以使用 resolve 模式:
// 1. 首次返回不带 edit 的轻量动作
connection.onRequest("textDocument/codeAction", (params) => {
return [
{
title: "Extract to method",
kind: "refactor.extract",
// 不包含 edit —— 延迟加载
},
];
});
// 2. 用户选中后再解析 edit
connection.onRequest("codeAction/resolve", async (action) => {
// 此时才计算具体的编辑内容
const edit = await computeExtractMethodEdit(action);
return { ...action, edit };
});
Server 声明:
{
"capabilities": {
"codeActionProvider": {
"codeActionKinds": ["quickfix", "refactor", "source"],
"resolveProvider": true
}
}
}
⚠️ 注意事项
| 问题 | 建议 |
|---|---|
| 动作太多 | 使用 context.only 过滤,只返回相关类型 |
| edit 与 command 顺序 | 先应用 edit,再执行 command |
| Code Lens 性能 | 使用 resolve 模式延迟加载 |
| 动作标题 | 使用清晰、简洁的标题 |
🔗 扩展阅读
下一章:第 9 章:代码格式化 — 全量格式化、范围格式化、保存时格式化。