LSP 跳转与高亮
定义跳转、引用查找、文档高亮、文档符号是 LSP 的「导航」能力:让用户快速理解代码结构与关联。本章在 Server 端实现全部导航特性。
onDefinition 定义跳转
Server 端处理定义跳转请求:
typescript
import {
createConnection,
TextDocuments,
Location
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
const connection = createConnection();
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// 注册定义跳转
connection.onDefinition((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return null;
}
const position = params.position;
// 获取当前位置的单词
const word = getWordAtPosition(document, position);
if (!word) {
return null;
}
// 在文档中查找定义
const def = findDefinition(document, word);
if (!def) {
return null;
}
// 返回定义位置
return {
uri: document.uri,
range: def
};
});返回结构
typescript
// 单个定义
{
uri: 'file:///doc.txt',
range: { start: { line: 5, character: 0 }, end: { line: 5, character: 10 } }
}
// 多个定义
[
{ uri: '...', range: { ... } },
{ uri: '...', range: { ... } }
]
// 定义链接(含预览范围)
{
targetUri: '...',
targetRange: { ... },
targetSelectionRange: { ... }
}onReferences 引用查找
typescript
connection.onReferences((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
const position = params.position;
const word = getWordAtPosition(document, position);
if (!word) {
return [];
}
const references: Location[] = [];
const text = document.getText();
const lines = text.split('\n');
// 遍历所有行查找引用
lines.forEach((lineText, line) => {
const regex = new RegExp(`\\b${word}\\b`, 'g');
let match: RegExpExecArray | null;
while ((match = regex.exec(lineText))) {
references.push({
uri: document.uri,
range: {
start: { line, character: match.index },
end: { line, character: match.index + word.length }
}
});
}
});
return references;
});ReferencesParams 上下文
typescript
connection.onReferences((params) => {
// 是否包含声明自身
const includeDeclaration = params.context.includeDeclaration;
// ...
});onDocumentHighlight 文档高亮
光标停留在标识符上时,高亮所有出现位置:
typescript
connection.onDocumentHighlight((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
const position = params.position;
const word = getWordAtPosition(document, position);
if (!word) {
return [];
}
const highlights: DocumentHighlight[] = [];
const text = document.getText();
const lines = text.split('\n');
lines.forEach((lineText, line) => {
const regex = new RegExp(`\\b${word}\\b`, 'g');
let match: RegExpExecArray | null;
while ((match = regex.exec(lineText))) {
highlights.push({
range: {
start: { line, character: match.index },
end: { line, character: match.index + word.length }
},
kind: DocumentHighlightKind.Read // 高亮类型
});
}
});
return highlights;
});DocumentHighlightKind
typescript
import { DocumentHighlightKind } from 'vscode-languageserver';
DocumentHighlightKind.Text // 1 文本高亮
DocumentHighlightKind.Read // 2 读取
DocumentHighlightKind.Write // 3 写入onDocumentSymbol 文档符号
提供文档大纲结构:
typescript
connection.onDocumentSymbol((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
const symbols: DocumentSymbol[] = [];
const text = document.getText();
const lines = text.split('\n');
lines.forEach((lineText, line) => {
const trimmed = lineText.trim();
// 解析实体定义 entity User {
const entityMatch = trimmed.match(/^entity\s+(\w+)\s*\{/);
if (entityMatch) {
symbols.push({
name: entityMatch[1],
kind: SymbolKind.Class,
range: {
start: { line, character: 0 },
end: { line, character: lineText.length }
},
selectionRange: {
start: { line, character: lineText.indexOf(entityMatch[1]) },
end: {
line,
character: lineText.indexOf(entityMatch[1]) + entityMatch[1].length
}
},
children: []
});
return;
}
// 解析字段定义 field name: string
const fieldMatch = trimmed.match(/^field\s+(\w+)/);
if (fieldMatch) {
symbols.push({
name: fieldMatch[1],
kind: SymbolKind.Field,
range: {
start: { line, character: 0 },
end: { line, character: lineText.length }
},
selectionRange: {
start: { line, character: lineText.indexOf(fieldMatch[1]) },
end: {
line,
character: lineText.indexOf(fieldMatch[1]) + fieldMatch[1].length
}
}
});
}
});
return symbols;
});DocumentSymbol 结构
| 字段 | 说明 |
|---|---|
name | 符号名 |
detail | 描述 |
kind | 类型(SymbolKind) |
range | 完整范围 |
selectionRange | 名称范围 |
children | 子符号 |
扁平符号(旧格式)
typescript
// SymbolInformation 扁平结构
return [
{
name: 'entity',
kind: SymbolKind.Class,
location: {
uri: document.uri,
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } }
}
}
];完整示例:DSL 导航器
typescript
import {
createConnection,
TextDocuments,
Location,
DocumentHighlight,
DocumentHighlightKind,
DocumentSymbol,
SymbolKind
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
const connection = createConnection();
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// 工具:获取位置单词
function getWordAtPosition(
document: TextDocument,
position: { line: number; character: number }
): string | undefined {
const lineText = document.getText({
start: { line: position.line, character: 0 },
end: { line: position.line, character: Number.MAX_SAFE_INTEGER }
});
const before = lineText.slice(0, position.character);
const match = before.match(/(\w+)$/);
return match ? match[1] : undefined;
}
// 工具:在文档中查找定义
function findDefinition(
document: TextDocument,
word: string
): { start: { line: number; character: number }; end: { line: number; character: number } } | null {
const text = document.getText();
const lines = text.split('\n');
// 匹配 "entity word {" 或 "field word:"
const defPattern = new RegExp(
`(?:entity|field)\\s+${word}\\b`
);
for (let line = 0; line < lines.length; line++) {
const match = defPattern.exec(lines[line]);
if (match) {
return {
start: { line, character: match.index },
end: { line, character: match.index + match[0].length }
};
}
}
return null;
}
// 声明能力
connection.onInitialize(() => ({
capabilities: {
textDocumentSync: { openClose: true, change: 2 },
definitionProvider: true,
referencesProvider: true,
documentHighlightProvider: true,
documentSymbolProvider: true
}
}));
// 定义跳转
connection.onDefinition((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return null;
const word = getWordAtPosition(document, params.position);
if (!word) return null;
const def = findDefinition(document, word);
if (!def) return null;
return { uri: document.uri, range: def };
});
// 引用查找
connection.onReferences((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return [];
const word = getWordAtPosition(document, params.position);
if (!word) return [];
const references: Location[] = [];
const lines = document.getText().split('\n');
lines.forEach((lineText, line) => {
const regex = new RegExp(`\\b${word}\\b`, 'g');
let match: RegExpExecArray | null;
while ((match = regex.exec(lineText))) {
references.push({
uri: document.uri,
range: {
start: { line, character: match.index },
end: { line, character: match.index + word.length }
}
});
}
});
return references;
});
// 文档高亮
connection.onDocumentHighlight((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return [];
const word = getWordAtPosition(document, params.position);
if (!word) return [];
const highlights: DocumentHighlight[] = [];
const lines = document.getText().split('\n');
lines.forEach((lineText, line) => {
const regex = new RegExp(`\\b${word}\\b`, 'g');
let match: RegExpExecArray | null;
while ((match = regex.exec(lineText))) {
highlights.push({
range: {
start: { line, character: match.index },
end: { line, character: match.index + word.length }
},
kind: DocumentHighlightKind.Read
});
}
});
return highlights;
});
// 文档符号
connection.onDocumentSymbol((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return [];
const symbols: DocumentSymbol[] = [];
const lines = document.getText().split('\n');
lines.forEach((lineText, line) => {
const trimmed = lineText.trim();
const match = trimmed.match(/^(entity|field)\s+(\w+)/);
if (match) {
const [, kind, name] = match;
symbols.push({
name,
detail: kind === 'entity' ? '实体' : '字段',
kind: kind === 'entity' ? SymbolKind.Class : SymbolKind.Field,
range: {
start: { line, character: 0 },
end: { line, character: lineText.length }
},
selectionRange: {
start: { line, character: lineText.indexOf(name) },
end: { line, character: lineText.indexOf(name) + name.length }
}
});
}
});
return symbols;
});
documents.listen(connection);
connection.listen();导航能力清单
| 特性 | 处理器 | 用户操作 |
|---|---|---|
| 定义跳转 | onDefinition | F12 |
| 引用查找 | onReferences | Shift+F12 |
| 文档高亮 | onDocumentHighlight | 光标停留 |
| 文档符号 | onDocumentSymbol | 大纲/面包屑 |
| 工作区符号 | onWorkspaceSymbol | 全局搜索符号 |
常见问题
| 问题 | 处理 |
|---|---|
| F12 无效 | 检查 capabilities 与返回结构 |
| 高亮消失 | 确认 DocumentHighlightKind |
| 大纲为空 | 检查 DocumentSymbol 结构 |
| 跨文件跳转 | 返回带完整 uri 的 Location |
跳转与高亮让 LSP Server 具备完整导航能力,下一章用实战把全部特性串成完整的 DSL 插件。