第 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 格式化选项
| 选项 | 类型 | 说明 |
|---|---|---|
tabSize | integer | Tab 宽度(空格数) |
insertSpaces | boolean | 用空格替代 Tab |
trimTrailingWhitespace | boolean | 去除行尾空白 |
insertFinalNewline | boolean | 文件末尾插入换行 |
trimFinalNewlines | boolean | 去除文件末尾多余换行 |
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 格式化器性能优化
| 优化策略 | 说明 |
|---|---|
| 增量格式化 | 只格式化变更的行 |
| 外部工具调用 | 调用 black、prettier 等外部格式化器 |
| 缓存结果 | 缓存未变更文档的格式化结果 |
| 异步处理 | 使用 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 实现。