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

LSP 开发指南 / 第 5 章:语言特性

5.1 语言特性总览

LSP 的核心价值在于提供统一的语言智能功能。本章聚焦最常用的五种特性:

特性方法名触发方式返回类型
代码补全textDocument/completion输入字符CompletionItem[]
悬停信息textDocument/hover鼠标悬停/快捷键Hover
跳转定义textDocument/definitionCtrl+Click / 快捷键Location | Location[]
引用查找textDocument/referencesShift+F12Location[]
签名帮助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说明
Invoked1用户手动触发(Ctrl+Space)
TriggerCharacter2输入了触发字符(如 .:
TriggerForIncompleteCompletions3之前的补全结果不完整,继续触发

5.2.3 CompletionItem 关键字段

字段类型说明
labelstring显示在补全列表中的文字
kindCompletionItemKind补全项的图标/类型
detailstring右侧附加信息(如类型签名)
documentationstring | MarkupContent详细文档
insertTextstring | Snippet插入的文本
insertTextFormat1 = PlainText, 2 = Snippet插入文本格式
sortTextstring排序依据(不显示,仅用于排序)
filterTextstring过滤依据(用户输入匹配此字段)
preselectboolean是否默认选中
commitCharactersstring[]确认字符列表

5.2.4 CompletionItemKind 枚举

名称图标典型用途
1TextT普通文本
2MethodM方法
3Functionƒ函数
4Constructor🔧构造函数
5Field🏷字段
6Variablex变量
7ClassC
8InterfaceI接口
9Module📦模块
10Property🏷属性
11Unit📏单位
12ValueV
13EnumE枚举
14KeywordK关键字
15Snippet📋代码片段
16Color🎨颜色
17File📄文件
18Reference📎引用
19Folder📁文件夹
20EnumMemberE枚举成员
21ConstantC常量
22StructS结构体
23Event事件
24OperatorO操作符
25TypeParameterT类型参数

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, $2Tab 停靠点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 章:诊断信息 — 错误与警告的发布、订阅及严重级别管理。