强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

LSP 开发指南 / 第 8 章:代码动作

8.1 代码动作概述

代码动作(Code Actions)是 LSP 中最灵活的特性之一,它将诊断修复、代码重构、代码透镜等统一到一个接口下。

8.1.1 代码动作类型

类型CodeActionKind触发方式典型用途
Quick Fixquickfix点击灯泡图标 / Ctrl+.修复诊断错误
RefactorrefactorCtrl+Shift+R重构操作
Sourcesource右键菜单全文件操作
Code LenscodeLens点击行内提示附加信息操作

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 章:代码格式化 — 全量格式化、范围格式化、保存时格式化。