LSP 诊断验证
诊断是 LSP 最基础也最常用的能力:Server 分析文档内容,主动推送错误/警告给客户端显示。本章实现完整的诊断链路。
诊断流程
文档打开/变更
↓
Server 收到通知(onDidOpenTextDocument / onDidChangeTextDocument)
↓
分析文档内容
↓
connection.sendDiagnostics({ uri, diagnostics })
↓
客户端渲染(波浪线 + 问题面板)触发诊断
文档打开时
typescript
// server.ts
import {
createConnection,
TextDocuments,
Diagnostic,
DiagnosticSeverity
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
const connection = createConnection();
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// 文档打开时校验
documents.onDidOpen((event) => {
validateDocument(event.document);
});
// 文档变更时校验
documents.onDidChangeContent((event) => {
validateDocument(event.document);
});
documents.listen(connection);
connection.listen();sendDiagnostics 推送诊断
typescript
function validateDocument(document: TextDocument): void {
const text = document.getText();
const diagnostics: Diagnostic[] = [];
// 解析每行
const lines = text.split('\n');
lines.forEach((lineText, line) => {
// 检查行过长
if (lineText.length > 100) {
diagnostics.push({
range: {
start: { line, character: 100 },
end: { line, character: lineText.length }
},
message: '行过长(超过 100 字符)',
severity: DiagnosticSeverity.Warning,
source: 'my-lsp'
});
}
// 检查未闭合括号
const openCount = (lineText.match(/\(/g) || []).length;
const closeCount = (lineText.match(/\)/g) || []).length;
if (openCount > closeCount) {
const idx = lineText.lastIndexOf('(');
diagnostics.push({
range: {
start: { line, character: idx },
end: { line, character: idx + 1 }
},
message: '括号未闭合',
severity: DiagnosticSeverity.Error,
source: 'my-lsp'
});
}
});
// 推送诊断
connection.sendDiagnostics({
uri: document.uri,
diagnostics
});
}Diagnostic 结构
| 字段 | 说明 |
|---|---|
range | 问题范围 |
message | 消息 |
severity | 严重程度 |
source | 来源 |
code | 错误码 |
Severity 映射
typescript
import { DiagnosticSeverity } from 'vscode-languageserver';
DiagnosticSeverity.Error // 1
DiagnosticSeverity.Warning // 2
DiagnosticSeverity.Information // 3
DiagnosticSeverity.Hint // 4诊断增量更新
版本跟踪
typescript
// 记录文档版本,避免过期诊断
const docVersions = new Map<string, number>();
function validateDocument(document: TextDocument): void {
docVersions.set(document.uri, document.version);
// 分析...
connection.sendDiagnostics({
uri: document.uri,
version: document.version, // 携带版本
diagnostics
});
}清空诊断
文档关闭时推送空数组清空:
typescript
documents.onDidClose((event) => {
// 清空已关闭文档的诊断
connection.sendDiagnostics({
uri: event.document.uri,
diagnostics: []
});
docVersions.delete(event.document.uri);
});防抖优化
频繁输入时避免重复分析:
typescript
let pendingValidation: NodeJS.Timeout | undefined;
documents.onDidChangeContent((event) => {
// 防抖:300ms 内合并变更
clearTimeout(pendingValidation);
pendingValidation = setTimeout(() => {
validateDocument(event.document);
}, 300);
});逐文档防抖
typescript
const debounceMap = new Map<string, NodeJS.Timeout>();
documents.onDidChangeContent((event) => {
const uri = event.document.uri;
// 取消该文档之前的定时器
const existing = debounceMap.get(uri);
if (existing) {
clearTimeout(existing);
}
debounceMap.set(uri, setTimeout(() => {
validateDocument(event.document);
debounceMap.delete(uri);
}, 300));
});配置驱动的诊断
typescript
// 配置控制诊断行为
let maxLineLength = 100;
let enableTrailingWhitespace = true;
connection.onDidChangeConfiguration((params) => {
const settings = params.settings;
if (settings?.myLsp) {
maxLineLength = settings.myLsp.maxLineLength ?? 100;
enableTrailingWhitespace =
settings.myLsp.enableTrailingWhitespace ?? true;
}
});
function validateDocument(document: TextDocument): void {
// 使用配置
if (lineText.length > maxLineLength) {
// ...
}
}完整示例:DSL 校验器
typescript
import {
createConnection,
TextDocuments,
Diagnostic,
DiagnosticSeverity
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
const connection = createConnection();
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// 关键字集合
const KEYWORDS = ['entity', 'field', 'rule', 'when', 'then'];
function validateDocument(document: TextDocument): void {
const text = document.getText();
const diagnostics: Diagnostic[] = [];
const lines = text.split('\n');
lines.forEach((lineText, line) => {
const trimmed = lineText.trim();
if (trimmed === '') return;
// 规则 1:语句必须以分号结尾
if (!trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}')) {
diagnostics.push({
range: {
start: { line, character: lineText.length },
end: { line, character: lineText.length }
},
message: '语句缺少分号',
severity: DiagnosticSeverity.Error,
source: 'dsl-checker'
});
}
// 规则 2:未知关键字
const firstWord = trimmed.split(/\s+/)[0];
if (!KEYWORDS.includes(firstWord) && !trimmed.startsWith('//')) {
const idx = lineText.indexOf(firstWord);
diagnostics.push({
range: {
start: { line, character: idx },
end: { line, character: idx + firstWord.length }
},
message: `未知关键字 "${firstWord}"`,
severity: DiagnosticSeverity.Warning,
source: 'dsl-checker'
});
}
// 规则 3:缩进必须是 2 空格倍数
const leadingSpaces = lineText.length - lineText.trimStart().length;
if (leadingSpaces % 2 !== 0 && trimmed !== '') {
diagnostics.push({
range: {
start: { line, character: 0 },
end: { line, character: leadingSpaces }
},
message: '缩进必须是 2 空格的倍数',
severity: DiagnosticSeverity.Hint,
source: 'dsl-checker'
});
}
});
connection.sendDiagnostics({
uri: document.uri,
diagnostics
});
}
documents.onDidOpen((event) => validateDocument(event.document));
documents.onDidChangeContent((event) => validateDocument(event.document));
documents.onDidClose((event) => {
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
});
documents.listen(connection);
connection.listen();Language Server 与普通 Provider 协作
LSP 与普通 Provider 可以共存:
| 场景 | 选择 |
|---|---|
| 简单特性(普通 Provider) | 悬停、简单补全 |
| 复杂特性(LSP) | 诊断、类型分析 |
| 混合 | 各取所长 |
typescript
// extension.ts:Client + 普通 Provider 共存
export function activate(context: vscode.ExtensionContext) {
// LSP Client
client.start();
// 普通补全 Provider(简单场景用本地实现)
vscode.languages.registerCompletionItemProvider(
'plaintext',
{
provideCompletionItems() {
return [new vscode.CompletionItem('quick')];
}
}
);
}常见问题
| 问题 | 处理 |
|---|---|
| 诊断不显示 | 检查 capabilities 与 sendDiagnostics |
| 诊断过期 | 携带 version 并增量更新 |
| 频繁闪烁 | 防抖优化 |
| 关闭后残留 | onDidClose 清空 |
诊断验证打通了 LSP 的核心链路,下一章实现补全与悬停等请求响应特性。