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

LSP 开发指南 / 第 9 章:代码格式化

9.1 格式化概述

LSP 提供三种格式化接口,覆盖不同的使用场景:

接口方法触发方式作用范围
全量格式化textDocument/formatting快捷键/菜单整个文档
范围格式化textDocument/rangeFormatting选中文本后格式化选中区域
键入时格式化textDocument/onTypeFormatting输入特定字符时当前位置附近
保存时格式化textDocument/willSaveWaitUntil保存文件时整个文档

9.2 全量格式化

9.2.1 请求与响应

Client 请求

{
  "jsonrpc": "2.0",
  "id": 40,
  "method": "textDocument/formatting",
  "params": {
    "textDocument": { "uri": "file:///src/main.py" },
    "options": {
      "tabSize": 4,
      "insertSpaces": true,
      "trimTrailingWhitespace": true,
      "insertFinalNewline": true,
      "trimFinalNewlines": true
    }
  }
}

Server 响应

{
  "jsonrpc": "2.0",
  "id": 40,
  "result": [
    {
      "range": {
        "start": { "line": 2, "character": 0 },
        "end": { "line": 2, "character": 12 }
      },
      "newText": "    "
    },
    {
      "range": {
        "start": { "line": 5, "character": 10 },
        "end": { "line": 5, "character": 10 }
      },
      "newText": "\n"
    }
  ]
}

9.2.2 格式化选项

选项类型说明
tabSizeintegerTab 宽度(空格数)
insertSpacesboolean用空格替代 Tab
trimTrailingWhitespaceboolean去除行尾空白
insertFinalNewlineboolean文件末尾插入换行
trimFinalNewlinesboolean去除文件末尾多余换行

9.2.3 实现示例

connection.onRequest("textDocument/formatting", (params) => {
  const doc = docManager.get(params.textDocument.uri);
  if (!doc) return [];

  const options = params.options;
  const edits: TextEdit[] = [];
  const lines = doc.text.split("\n");

  for (let i = 0; i < lines.length; i++) {
    let line = lines[i];

    // 1. 修复缩进
    const indentMatch = line.match(/^(\s*)/);
    const currentIndent = indentMatch ? indentMatch[1] : "";
    const correctIndent = calculateCorrectIndent(doc.text, i, options);

    if (currentIndent !== correctIndent) {
      edits.push({
        range: {
          start: { line: i, character: 0 },
          end: { line: i, character: currentIndent.length },
        },
        newText: correctIndent,
      });
    }

    // 2. 去除行尾空白
    if (options.trimTrailingWhitespace && /\s+$/.test(line)) {
      const trimmed = line.trimEnd();
      edits.push({
        range: {
          start: { line: i, character: trimmed.length },
          end: { line: i, character: line.length },
        },
        newText: "",
      });
    }

    // 3. 运算符两侧空格
    line = line.replace(/(\w)\s*(=|==|!=|<=|>=|\+|-|\*|\/)\s*(\w)/g, "$1 $2 $3");

    // 4. 逗号后空格
    line = line.replace(/,(\S)/g, ", $1");
  }

  // 5. 文件末尾换行
  if (options.insertFinalNewline && !doc.text.endsWith("\n")) {
    const lastLine = lines.length - 1;
    edits.push({
      range: {
        start: { line: lastLine, character: lines[lastLine].length },
        end: { line: lastLine, character: lines[lastLine].length },
      },
      newText: "\n",
    });
  }

  return edits;
});

9.3 范围格式化

9.3.1 请求与响应

Client 请求

{
  "jsonrpc": "2.0",
  "id": 41,
  "method": "textDocument/rangeFormatting",
  "params": {
    "textDocument": { "uri": "file:///src/main.py" },
    "range": {
      "start": { "line": 10, "character": 0 },
      "end": { "line": 20, "character": 0 }
    },
    "options": {
      "tabSize": 4,
      "insertSpaces": true
    }
  }
}

9.3.2 实现要点

connection.onRequest("textDocument/rangeFormatting", (params) => {
  const doc = docManager.get(params.textDocument.uri);
  if (!doc) return [];

  const startLine = params.range.start.line;
  const endLine = params.range.end.line;
  const edits: TextEdit[] = [];
  const lines = doc.text.split("\n");

  // 只格式化指定范围内的行
  for (let i = startLine; i <= endLine && i < lines.length; i++) {
    const line = lines[i];

    // 应用格式化规则
    const formatted = formatLine(line, params.options);
    if (formatted !== line) {
      edits.push({
        range: {
          start: { line: i, character: 0 },
          end: { line: i, character: line.length },
        },
        newText: formatted,
      });
    }
  }

  // 将选区扩展到完整语句(可选)
  return expandEditsToFullStatements(edits, lines, startLine, endLine);
});

9.4 键入时格式化

9.4.1 触发字符

Server 在初始化时声明触发字符:

{
  "capabilities": {
    "documentOnTypeFormattingProvider": {
      "firstTriggerCharacter": "\n",
      "moreTriggerCharacter": [";", "}", ")"]
    }
  }
}

9.4.2 请求格式

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "textDocument/onTypeFormatting",
  "params": {
    "textDocument": { "uri": "file:///src/main.py" },
    "position": { "line": 15, "character": 0 },
    "ch": "\n",
    "options": {
      "tabSize": 4,
      "insertSpaces": true
    }
  }
}

9.4.3 自动缩进实现

connection.onRequest("textDocument/onTypeFormatting", (params) => {
  const doc = docManager.get(params.textDocument.uri);
  if (!doc) return [];

  const { position, ch, options } = params;
  const lines = doc.text.split("\n");
  const edits: TextEdit[] = [];

  if (ch === "\n") {
    // 自动缩进:根据上一行推断新行缩进
    const prevLine = lines[position.line - 1] || "";
    const indentMatch = prevLine.match(/^(\s*)/);
    let indent = indentMatch ? indentMatch[1] : "";

    // 如果上一行以 { : ( [ 结尾,增加缩进
    if (/[{(:\[]\s*$/.test(prevLine.trim())) {
      indent += options.insertSpaces ? " ".repeat(options.tabSize) : "\t";
    }

    // 如果上一行以 ) 或 } 结尾,可能需要减少缩进
    if (/^\s*[})\]]/.test(lines[position.line])) {
      indent = indent.substring(0, indent.length - options.tabSize);
    }

    // 在新行开头插入正确缩进
    const currentLine = lines[position.line] || "";
    const currentIndent = currentLine.match(/^(\s*)/)?.[1] || "";

    if (currentIndent !== indent) {
      edits.push({
        range: {
          start: { line: position.line, character: 0 },
          end: { line: position.line, character: currentIndent.length },
        },
        newText: indent,
      });
    }
  } else if (ch === "}") {
    // 输入 } 时对齐到对应的 {
    const openBraceLine = findMatchingBrace(lines, position.line, "}");
    if (openBraceLine !== -1) {
      const openIndent = lines[openBraceLine].match(/^(\s*)/)?.[1] || "";
      const currentIndent = lines[position.line].match(/^(\s*)/)?.[1] || "";

      if (currentIndent !== openIndent) {
        edits.push({
          range: {
            start: { line: position.line, character: 0 },
            end: { line: position.line, character: currentIndent.length },
          },
          newText: openIndent,
        });
      }
    }
  }

  return edits;
});

9.5 保存时格式化

9.5.1 配置方式

VS Code settings.json

{
  "editor.formatOnSave": true,
  "[python]": {
    "editor.defaultFormatter": "my-lsp-extension",
    "editor.formatOnSave": true
  }
}

9.5.2 Server 端处理

// 方式 1:通过 willSaveWaitUntil
connection.onRequest("textDocument/willSaveWaitUntil", async (params) => {
  if (params.reason !== SaveReason.Manual) return [];

  const doc = docManager.get(params.textDocument.uri);
  if (!doc) return [];

  return formatDocument(doc);
});

// 方式 2:通过配置控制
connection.onNotification("textDocument/didSave", async (params) => {
  const config = await getConfiguration();
  if (config.formatOnSave) {
    const doc = docManager.get(params.textDocument.uri);
    if (!doc) return;

    const edits = formatDocument(doc);
    if (edits.length > 0) {
      // 通过 applyEdit 请求 Client 应用编辑
      await connection.sendRequest("workspace/applyEdit", {
        edit: {
          changes: {
            [params.textDocument.uri]: edits,
          },
        },
      });
    }
  }
});

9.6 多格式化器集成

在实际项目中,可能同时使用多个格式化工具:

interface Formatter {
  name: string;
  supportedLanguages: string[];
  format(text: string, options: FormatOptions): string;
}

class FormatterChain {
  private formatters: Formatter[] = [];

  add(formatter: Formatter): void {
    this.formatters.push(formatter);
  }

  format(text: string, languageId: string, options: FormatOptions): string {
    let result = text;
    for (const formatter of this.formatters) {
      if (formatter.supportedLanguages.includes(languageId)) {
        result = formatter.format(result, options);
      }
    }
    return result;
  }
}

// 配置格式化器链
const formatters = new FormatterChain();
formatters.add(new ImportSorter());
formatters.add(new IndentFixer());
formatters.add(new TrailingWhitespaceRemover());

connection.onRequest("textDocument/formatting", (params) => {
  const doc = docManager.get(params.textDocument.uri);
  if (!doc) return [];

  const formatted = formatters.format(doc.text, doc.languageId, params.options);
  return computeTextEdits(doc.text, formatted);
});

9.7 格式化器性能优化

优化策略说明
增量格式化只格式化变更的行
外部工具调用调用 blackprettier 等外部格式化器
缓存结果缓存未变更文档的格式化结果
异步处理使用 worker thread 避免阻塞主进程
import { execSync } from "child_process";

// 调用外部格式化器
function formatWithExternalTool(
  text: string,
  tool: string,
  args: string[]
): string {
  try {
    const result = execSync(
      `echo ${JSON.stringify(text)} | ${tool} ${args.join(" ")}`,
      { encoding: "utf-8", timeout: 5000 }
    );
    return result;
  } catch (err) {
    console.error(`Format tool ${tool} failed:`, err);
    return text; // 失败时返回原文
  }
}

// Python: black
const formatted = formatWithExternalTool(text, "black", ["--line-length", "88", "-"]);

// TypeScript: prettier
const formatted = formatWithExternalTool(text, "prettier", ["--parser", "typescript"]);

⚠️ 注意事项

问题建议
格式化导致光标跳动正确计算 Range 避免不必要的变更
与内置格式化器冲突在配置中明确指定默认格式化器
性能问题使用外部工具而非自行实现
行尾换行符统一使用 \n,由 Client 负责转换

🔗 扩展阅读


下一章第 10 章:实现示例 — TypeScript、Python、Go 完整 Language Server 实现。