LSP 补全与悬停
补全(Completion)与悬停(Hover)是 LSP 中典型的「请求-响应」特性:客户端发请求,Server 计算并返回结果。本章在 Server 端实现这两个能力。
onCompletion 补全处理
Server 端注册补全处理器:
typescript
// server.ts
import {
createConnection,
TextDocuments,
CompletionItem,
CompletionItemKind
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
const connection = createConnection();
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// 注册补全处理器
connection.onCompletion((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
// 获取光标位置
const position = params.position;
const lineText = document.getText({
start: { line: position.line, character: 0 },
end: position
});
// 提取当前输入前缀
const match = lineText.match(/(\w+)$/);
const prefix = match ? match[1] : '';
// 过滤补全项
return SUGGESTIONS
.filter((s) => s.label.startsWith(prefix))
.map((s) => s.item);
});
documents.listen(connection);
connection.listen();CompletionItem Server 端构建
typescript
// 补全数据源
const SUGGESTIONS = [
{
label: 'entity',
detail: '定义实体',
documentation: '创建数据实体\n\n```\nentity User {\n name: string\n}\n```',
kind: CompletionItemKind.Keyword
},
{
label: 'field',
detail: '定义字段',
documentation: '为实体添加字段',
kind: CompletionItemKind.Keyword
},
{
label: 'rule',
detail: '定义规则',
documentation: '创建业务规则',
kind: CompletionItemKind.Keyword
}
];
// 构建 CompletionItem
function buildCompletionItem(source: typeof SUGGESTIONS[0]): CompletionItem {
return {
label: source.label,
kind: source.kind,
detail: source.detail,
documentation: source.documentation,
sortText: '0', // 排前
insertText: source.label // 插入文本
};
}CompletionItem 关键字段
| 字段 | 作用 |
|---|---|
label | 显示名 |
kind | 类型(图标) |
detail | 描述 |
documentation | 文档 |
insertText | 插入文本 |
insertTextFormat | 纯文本或 Snippet |
sortText | 排序 |
preselect | 默认选中 |
模板补全(Snippet)
typescript
// 启用 Snippet 格式
import { InsertTextFormat } from 'vscode-languageserver';
const item: CompletionItem = {
label: 'entity-block',
kind: CompletionItemKind.Snippet,
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'entity ${1:Name} {\n\t${2}\n}',
documentation: '实体模板'
};onCompletionResolve 补全解析
补全项详情可延迟提供(客户端请求 resolve):
typescript
// 声明 resolve 支持(在 capabilities 中)
capabilities: {
completionProvider: {
triggerCharacters: ['.'],
resolveProvider: true // 支持补全项解析
}
}
// 补全时返回轻量项
connection.onCompletion((params) => {
return [
{
label: 'entity',
kind: CompletionItemKind.Keyword,
data: 'entity' // 标识符,供 resolve 查找
}
];
});
// 补全项解析:补充详情
connection.onCompletionResolve((item) => {
const doc = DOCUMENTATION_MAP[item.data];
if (doc) {
item.documentation = doc;
item.detail = doc.summary;
}
return item;
});onHover 悬停内容生成
Server 端实现悬停:
typescript
// 注册悬停处理器
connection.onHover((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return null;
}
const position = params.position;
const lineText = document.getText({
start: { line: position.line, character: 0 },
end: { line: position.line, character: Number.MAX_SAFE_INTEGER }
});
// 提取悬停位置的单词
const wordMatch = lineText.slice(0, position.character).match(/(\w+)$/);
if (!wordMatch) {
return null;
}
const word = wordMatch[1];
// 查找悬停文档
const doc = KEYWORD_DOCS[word];
if (!doc) {
return null;
}
// 返回 Hover 结果
return {
contents: {
kind: 'markdown',
value: `**${word}**\n\n${doc}`
},
range: {
start: {
line: position.line,
character: position.character - word.length
},
end: position
}
};
});Hover 返回结构
typescript
{
contents: '纯文本' | { kind: 'markdown', value: '...' },
range: { start: Position, end: Position }
}请求上下文
CompletionParams
| 字段 | 说明 |
|---|---|
textDocument.uri | 文档 URI |
position | 光标位置 |
context.triggerKind | 触发类型 |
context.triggerCharacter | 触发字符 |
typescript
import { CompletionTriggerKind } from 'vscode-languageserver';
connection.onCompletion((params) => {
const triggerKind = params.context?.triggerKind;
const triggerChar = params.context?.triggerCharacter;
// 判断触发方式
if (triggerChar === '.') {
return this.getMemberCompletions(params);
}
if (triggerChar === ':') {
return this.getTypeCompletions(params);
}
if (triggerKind === CompletionTriggerKind.Invoked) {
return this.getAllCompletions(params);
}
return [];
});HoverParams
| 字段 | 说明 |
|---|---|
textDocument.uri | 文档 URI |
position | 悬停位置 |
完整示例:DSL 补全 + 悬停
typescript
import {
createConnection,
TextDocuments,
CompletionItem,
CompletionItemKind,
InsertTextFormat
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
const connection = createConnection();
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// 关键字文档库
const KEYWORDS: Record<string, string> = {
'entity': '定义一个数据实体。\n\n`entity User { ... }`',
'field': '为实体添加字段。\n\n`field name: string`',
'rule': '定义业务规则。\n\n`rule 超时 { when ... then ... }`',
'when': '规则条件关键字。',
'then': '规则动作关键字。'
};
connection.onInitialize(() => ({
capabilities: {
textDocumentSync: { openClose: true, change: 2 },
completionProvider: {
triggerCharacters: ['.', ':'],
resolveProvider: true
},
hoverProvider: true
}
}));
// 补全
connection.onCompletion((params): CompletionItem[] => {
const document = documents.get(params.textDocument.uri);
if (!document) return [];
const position = params.position;
const before = document.getText({
start: { line: position.line, character: 0 },
end: position
});
const match = before.match(/(\w+)$/);
const prefix = match ? match[1] : '';
return Object.keys(KEYWORDS)
.filter((key) => key.startsWith(prefix))
.map((key, index) => ({
label: key,
kind: CompletionItemKind.Keyword,
detail: key === 'entity' ? '实体定义' : '关键字',
sortText: index.toString(),
data: key // 供 resolve 使用
}));
});
// 补全详情解析
connection.onCompletionResolve((item) => {
const doc = KEYWORDS[item.data];
if (doc) {
item.documentation = { kind: 'markdown', value: doc };
item.detail = 'DSL 关键字';
}
return item;
});
// 悬停
connection.onHover((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return null;
const position = params.position;
const lineText = document.getText({
start: { line: position.line, character: 0 },
end: { line: position.line, character: Number.MAX_SAFE_INTEGER }
});
const match = lineText.slice(0, position.character).match(/(\w+)$/);
if (!match) return null;
const word = match[1];
const doc = KEYWORDS[word];
if (!doc) return null;
return {
contents: {
kind: 'markdown',
value: `### \`${word}\`\n\n${doc}`
},
range: {
start: {
line: position.line,
character: position.character - word.length
},
end: position
}
};
});
documents.listen(connection);
connection.listen();常见问题
| 问题 | 处理 |
|---|---|
| 补全不弹出 | 检查 capabilities 与 triggerCharacters |
| 悬停无内容 | 确认 onHover 返回结构正确 |
| 位置偏移 | 精确计算前缀与范围 |
| resolve 不调用 | 确认 resolveProvider: true |
补全与悬停让 LSP Server 提供「智能输入」能力,下一章实现跳转与高亮。