LSP 开发指南 / 第 5 章:语言特性
5.1 语言特性总览
LSP 的核心价值在于提供统一的语言智能功能。本章聚焦最常用的五种特性:
| 特性 | 方法名 | 触发方式 | 返回类型 |
|---|---|---|---|
| 代码补全 | textDocument/completion | 输入字符 | CompletionItem[] |
| 悬停信息 | textDocument/hover | 鼠标悬停/快捷键 | Hover |
| 跳转定义 | textDocument/definition | Ctrl+Click / 快捷键 | Location | Location[] |
| 引用查找 | textDocument/references | Shift+F12 | Location[] |
| 签名帮助 | textDocument/signatureHelp | 输入 ( | SignatureHelp |
5.2 代码补全(Completion)
5.2.1 请求与响应
Client 请求:
{
"jsonrpc": "2.0",
"id": 10,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": "file:///src/main.py" },
"position": { "line": 5, "character": 12 },
"context": {
"triggerKind": 2,
"triggerCharacter": "."
}
}
}
Server 响应:
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"isIncomplete": false,
"items": [
{
"label": "append",
"kind": 2,
"detail": "(method) append(object) -> None",
"documentation": "Append object to the end of the list.",
"insertText": "append(${1:object})",
"insertTextFormat": 2,
"sortText": "0001",
"filterText": "append",
"commitCharacters": ["(", ";"]
}
]
}
}
5.2.2 补全触发方式
| 触发方式 | triggerKind 值 | 说明 |
|---|---|---|
| Invoked | 1 | 用户手动触发(Ctrl+Space) |
| TriggerCharacter | 2 | 输入了触发字符(如 .、:) |
| TriggerForIncompleteCompletions | 3 | 之前的补全结果不完整,继续触发 |
5.2.3 CompletionItem 关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
label | string | 显示在补全列表中的文字 |
kind | CompletionItemKind | 补全项的图标/类型 |
detail | string | 右侧附加信息(如类型签名) |
documentation | string | MarkupContent | 详细文档 |
insertText | string | Snippet | 插入的文本 |
insertTextFormat | 1 = PlainText, 2 = Snippet | 插入文本格式 |
sortText | string | 排序依据(不显示,仅用于排序) |
filterText | string | 过滤依据(用户输入匹配此字段) |
preselect | boolean | 是否默认选中 |
commitCharacters | string[] | 确认字符列表 |
5.2.4 CompletionItemKind 枚举
| 值 | 名称 | 图标 | 典型用途 |
|---|---|---|---|
| 1 | Text | T | 普通文本 |
| 2 | Method | M | 方法 |
| 3 | Function | ƒ | 函数 |
| 4 | Constructor | 🔧 | 构造函数 |
| 5 | Field | 🏷 | 字段 |
| 6 | Variable | x | 变量 |
| 7 | Class | C | 类 |
| 8 | Interface | I | 接口 |
| 9 | Module | 📦 | 模块 |
| 10 | Property | 🏷 | 属性 |
| 11 | Unit | 📏 | 单位 |
| 12 | Value | V | 值 |
| 13 | Enum | E | 枚举 |
| 14 | Keyword | K | 关键字 |
| 15 | Snippet | 📋 | 代码片段 |
| 16 | Color | 🎨 | 颜色 |
| 17 | File | 📄 | 文件 |
| 18 | Reference | 📎 | 引用 |
| 19 | Folder | 📁 | 文件夹 |
| 20 | EnumMember | E | 枚举成员 |
| 21 | Constant | C | 常量 |
| 22 | Struct | S | 结构体 |
| 23 | Event | ⚡ | 事件 |
| 24 | Operator | O | 操作符 |
| 25 | TypeParameter | T | 类型参数 |
5.2.5 补全实现示例
connection.onRequest("textDocument/completion", (params) => {
const doc = docManager.get(params.textDocument.uri);
if (!doc) return { isIncomplete: false, items: [] };
const line = doc.text.split("\n")[params.position.line];
const prefix = line.substring(0, params.position.character);
// 获取当前正在输入的单词
const wordMatch = prefix.match(/(\w+)$/);
const word = wordMatch ? wordMatch[1] : "";
// 触发字符上下文
const triggerChar = params.context?.triggerCharacter;
let items: CompletionItem[] = [];
if (triggerChar === ".") {
// 对象成员补全
items = getMemberCompletions(doc, params.position);
} else if (triggerChar === ":") {
// 类型注解补全
items = getTypeCompletions(doc, params.position);
} else {
// 通用补全
items = [
...getLocalSymbols(doc, params.position),
...getBuiltinSymbols(),
];
}
// 根据输入前缀过滤
if (word) {
items = items.filter((item) =>
item.label.toLowerCase().startsWith(word.toLowerCase())
);
}
return {
isIncomplete: false,
items,
};
});
// 补全项解析(惰性加载详情)
connection.onRequest("completionItem/resolve", (item) => {
// 只有用户选中某项后才加载完整文档
if (!item.documentation) {
item.documentation = loadFullDocumentation(item.label);
}
return item;
});
5.2.6 Snippet 语法
补全支持 VS Code Snippet 语法:
| 语法 | 说明 | 示例 |
|---|---|---|
$1, $2 | Tab 停靠点 | func(${1:name}) |
${1:default} | 带默认值的停靠点 | ${1:int} ${2:name} |
$0 | 最终光标位置 | {\n $0\n} |
$TM_FILENAME | 当前文件名 | $TM_FILENAME |
${1|opt1,opt2|} | 选择列表 | ${1|public,private|} |
// Python def 模板
const snippet: CompletionItem = {
label: "def",
kind: CompletionItemKind.Snippet,
detail: "Define a function",
insertText: "def ${1:function_name}(${2:params}):\n \"\"\"${3:Docstring}\"\"\"\n $0",
insertTextFormat: InsertTextFormat.Snippet,
documentation: {
kind: MarkupKind.Markdown,
value: "```python\ndef function_name(params):\n \"\"\"Docstring\"\"\"\n pass\n```",
},
};
5.3 悬停信息(Hover)
5.3.1 请求与响应
Client 请求:
{
"jsonrpc": "2.0",
"id": 11,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": "file:///src/main.py" },
"position": { "line": 5, "character": 8 }
}
}
Server 响应:
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"contents": {
"kind": "markdown",
"value": "```python\ndef calculate_sum(a: int, b: int) -> int\n```\n\n---\n\nCalculate the sum of two integers.\n\n**Parameters:**\n- `a` - First integer\n- `b` - Second integer\n\n**Returns:** The sum of a and b"
},
"range": {
"start": { "line": 5, "character": 4 },
"end": { "line": 5, "character": 17 }
}
}
}
5.3.2 实现示例
connection.onRequest("textDocument/hover", (params) => {
const doc = docManager.get(params.textDocument.uri);
if (!doc) return null;
const symbol = findSymbolAtPosition(doc, params.position);
if (!symbol) return null;
// 构建 Markdown 悬停内容
let markdown = "";
if (symbol.kind === "function") {
markdown = buildFunctionHover(symbol);
} else if (symbol.kind === "variable") {
markdown = buildVariableHover(symbol);
} else if (symbol.kind === "class") {
markdown = buildClassHover(symbol);
}
if (!markdown) return null;
return {
contents: {
kind: "markdown",
value: markdown,
},
range: symbol.range,
};
});
function buildFunctionHover(fn: SymbolInfo): string {
const signature = `\`\`\`python\ndef ${fn.name}(${fn.parameters
.map((p) => `${p.name}: ${p.type}`)
.join(", ")}) -> ${fn.returnType}\n\`\`\``;
const description = fn.documentation
? `\n---\n\n${fn.documentation}`
: "";
return signature + description;
}
5.4 跳转定义(Definition)
5.4.1 请求与响应
Client 请求:
{
"jsonrpc": "2.0",
"id": 12,
"method": "textDocument/definition",
"params": {
"textDocument": { "uri": "file:///src/main.py" },
"position": { "line": 10, "character": 4 }
}
}
Server 响应:
{
"jsonrpc": "2.0",
"id": 12,
"result": [
{
"uri": "file:///src/utils.py",
"range": {
"start": { "line": 24, "character": 0 },
"end": { "line": 24, "character": 30 }
}
}
]
}
5.4.2 相关导航方法
| 方法 | 说明 | 快捷键 (VS Code) |
|---|---|---|
textDocument/definition | 跳转到定义 | F12 |
textDocument/typeDefinition | 跳转到类型定义 | Ctrl+Shift+F10 |
textDocument/declaration | 跳转到声明 | — |
textDocument/implementation | 跳转到实现 | Ctrl+F12 |
textDocument/references | 查找所有引用 | Shift+F12 |
5.4.3 实现示例
connection.onRequest("textDocument/definition", (params) => {
const doc = docManager.get(params.textDocument.uri);
if (!doc) return null;
const word = getWordAtPosition(doc.text, params.position);
if (!word) return null;
// 在所有文档中查找定义
const definitions: Location[] = [];
for (const [uri, otherDoc] of docManager.getAllDocuments()) {
const def = findDefinition(otherDoc, word);
if (def) {
definitions.push({ uri, range: def.range });
}
}
// 如果在同一文件中有引用,排除自身位置
return definitions.filter((loc) => {
if (loc.uri !== params.textDocument.uri) return true;
const pos = params.position;
const start = loc.range.start;
const end = loc.range.end;
return !(pos.line === start.line && pos.character >= start.character && pos.character <= end.character);
});
});
function findDefinition(doc: TextDocumentItem, word: string): { range: Range } | null {
const lines = doc.text.split("\n");
for (let i = 0; i < lines.length; i++) {
// 简单匹配:查找 def/class/var 定义
const patterns = [
new RegExp(`^(def|function)\\s+${word}\\b`),
new RegExp(`^(class|interface)\\s+${word}\\b`),
new RegExp(`^${word}\\s*[=:]`),
];
for (const pattern of patterns) {
if (pattern.test(lines[i])) {
const charStart = lines[i].indexOf(word);
return {
range: {
start: { line: i, character: charStart },
end: { line: i, character: charStart + word.length },
},
};
}
}
}
return null;
}
5.5 引用查找(References)
5.5.1 请求与响应
Client 请求:
{
"jsonrpc": "2.0",
"id": 13,
"method": "textDocument/references",
"params": {
"textDocument": { "uri": "file:///src/main.py" },
"position": { "line": 5, "character": 4 },
"context": {
"includeDeclaration": true
}
}
}
Server 响应:
{
"jsonrpc": "2.0",
"id": 13,
"result": [
{
"uri": "file:///src/main.py",
"range": {
"start": { "line": 5, "character": 4 },
"end": { "line": 5, "character": 15 }
}
},
{
"uri": "file:///src/main.py",
"range": {
"start": { "line": 20, "character": 8 },
"end": { "line": 20, "character": 19 }
}
},
{
"uri": "file:///src/utils.py",
"range": {
"start": { "line": 42, "character": 12 },
"end": { "line": 42, "character": 23 }
}
}
]
}
5.5.2 实现示例
connection.onRequest("textDocument/references", (params) => {
const doc = docManager.get(params.textDocument.uri);
if (!doc) return [];
const word = getWordAtPosition(doc.text, params.position);
if (!word) return [];
const references: Location[] = [];
// 在所有已打开的文档中搜索
for (const [uri, otherDoc] of docManager.getAllDocuments()) {
const lines = otherDoc.text.split("\n");
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
let colIdx = 0;
const lineText = lines[lineIdx];
while (colIdx < lineText.length) {
const matchIdx = lineText.indexOf(word, colIdx);
if (matchIdx === -1) break;
// 验证是完整单词匹配
const before = matchIdx > 0 ? lineText[matchIdx - 1] : " ";
const after = matchIdx + word.length < lineText.length
? lineText[matchIdx + word.length]
: " ";
if (/\w/.test(before) || /\w/.test(after)) {
colIdx = matchIdx + 1;
continue;
}
references.push({
uri,
range: {
start: { line: lineIdx, character: matchIdx },
end: { line: lineIdx, character: matchIdx + word.length },
},
});
colIdx = matchIdx + word.length;
}
}
}
return references;
});
5.6 签名帮助(Signature Help)
5.6.1 请求与响应
Client 请求(当用户输入 ( 或 , 时触发):
{
"jsonrpc": "2.0",
"id": 14,
"method": "textDocument/signatureHelp",
"params": {
"textDocument": { "uri": "file:///src/main.py" },
"position": { "line": 10, "character": 25 },
"context": {
"triggerKind": 2,
"triggerCharacter": ",",
"isRetrigger": false
}
}
}
Server 响应:
{
"jsonrpc": "2.0",
"id": 14,
"result": {
"signatures": [
{
"label": "calculate(a: int, b: int, c: int) -> int",
"documentation": "Perform a three-way calculation",
"parameters": [
{
"label": [10, 22],
"documentation": "First parameter"
},
{
"label": [24, 36],
"documentation": "Second parameter"
},
{
"label": [38, 50],
"documentation": "Third parameter"
}
],
"activeParameter": 1
}
],
"activeSignature": 0,
"activeParameter": 1
}
}
5.6.2 实现示例
connection.onRequest("textDocument/signatureHelp", (params) => {
const doc = docManager.get(params.textDocument.uri);
if (!doc) return null;
const { line, character } = params.position;
const lines = doc.text.split("\n");
const lineText = lines[line];
// 从光标位置向前搜索函数调用
const funcInfo = findEnclosingFunctionCall(lineText, character);
if (!funcInfo) return null;
// 查找函数定义
const funcDef = findFunctionDefinition(doc, funcInfo.name);
if (!funcDef) return null;
const signatureLabel = buildSignatureLabel(funcDef);
return {
signatures: [
{
label: signatureLabel,
documentation: funcDef.documentation ?? "",
parameters: funcDef.parameters.map((p) => ({
label: `${p.name}: ${p.type}`,
documentation: p.documentation ?? "",
})),
},
],
activeSignature: 0,
activeParameter: funcInfo.currentParamIndex,
};
});
interface FuncCallInfo {
name: string;
currentParamIndex: number;
}
function findEnclosingFunctionCall(lineText: string, cursorPos: number): FuncCallInfo | null {
let depth = 0;
let paramIndex = 0;
// 从光标位置向后回溯
for (let i = cursorPos - 1; i >= 0; i--) {
const ch = lineText[i];
if (ch === ")") {
depth++;
} else if (ch === "(") {
if (depth === 0) {
// 找到了对应的左括号
const beforeParen = lineText.substring(0, i).trim();
const funcMatch = beforeParen.match(/(\w+)\s*$/);
if (funcMatch) {
return { name: funcMatch[1], currentParamIndex: paramIndex };
}
return null;
}
depth--;
} else if (ch === "," && depth === 0) {
paramIndex++;
}
}
return null;
}
5.6.3 触发字符配置
Server 在初始化时声明签名帮助的触发字符:
{
"capabilities": {
"signatureHelpProvider": {
"triggerCharacters": ["(", ",", ")"],
"retriggerCharacters": ["."]
}
}
}
| 触发字符 | 说明 |
|---|---|
( | 进入函数调用 |
, | 切换到下一个参数 |
) | 可选,重新触发以显示最终状态 |
5.7 类型层次(Type Hierarchy)
LSP 3.17 引入类型层次特性:
// 请求类型层次
connection.onRequest("typeHierarchy/supertypes", async (params) => {
const className = findClassAtPosition(params.item);
if (!className) return [];
const superClass = findSuperClass(className);
return superClass
? [{ name: superClass.name, kind: SymbolKind.Class, uri: superClass.uri, range: superClass.range }]
: [];
});
connection.onRequest("typeHierarchy/subtypes", async (params) => {
const className = findClassAtPosition(params.item);
if (!className) return [];
return findSubClasses(className).map((sub) => ({
name: sub.name,
kind: SymbolKind.Class,
uri: sub.uri,
range: sub.range,
}));
});
⚠️ 注意事项
| 问题 | 建议 |
|---|---|
| 补全响应太慢 | 设置 isIncomplete: true 并使用 debounce |
| 跳转定义找不到 | 优先使用项目级符号索引而非逐文件搜索 |
| 引用查找范围过大 | 限制搜索深度,使用进度报告 |
| 签名帮助嵌套函数 | 需要正确的括号深度计算 |
| 悬停内容过长 | 控制在合理长度,使用折叠 |
🔗 扫展阅读
下一章:第 6 章:诊断信息 — 错误与警告的发布、订阅及严重级别管理。