实战:自定义 DSL 语言支持插件
把前三章 LSP 能力综合运用,从零搭建一个完整的 DSL 语言支持插件:Client + Server 双进程,实现关键字高亮、诊断、补全、跳转、悬停全部语言特性。
目标
实现一个简单的配置 DSL(领域特定语言):
// 配置文件示例
entity User {
field name: string
field age: number
}
rule 校验规则 {
when age > 18
then approve
}第一步:项目结构
dsl-extension/
├── package.json # 插件入口
├── tsconfig.json
├── client/
│ ├── package.json # Client 依赖
│ └── src/
│ └── extension.ts # Client 激活
└── server/
├── package.json # Server 依赖
└── src/
└── server.ts # Server 实现第二步:插件 package.json
json
{
"name": "dsl-language-support",
"displayName": "DSL Language Support",
"version": "0.1.0",
"publisher": "your-name",
"engines": { "vscode": "^1.85.0" },
"categories": ["Programming Languages"],
"activationEvents": [
"onLanguage:dsl"
],
"main": "./client/out/extension",
"contributes": {
"languages": [
{
"id": "dsl",
"aliases": ["DSL", "dsl"],
"extensions": [".dsl"],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "dsl",
"scopeName": "source.dsl",
"path": "./syntaxes/dsl.tmLanguage.json"
}
],
"configuration": {
"title": "DSL",
"properties": {
"dsl.maxLineLength": {
"type": "number",
"default": 100,
"description": "行长度上限"
}
}
}
},
"scripts": {
"compile": "tsc -p client && tsc -p server",
"watch": "tsc -w -p client & tsc -w -p server"
}
}第三步:Client 端
client/src/extension.ts:
typescript
import * as path from 'path';
import * as vscode from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind
} from 'vscode-languageclient/node';
let client: LanguageClient;
export function activate(context: vscode.ExtensionContext) {
// Server 模块路径
const serverModule = context.asAbsolutePath(
path.join('server', 'out', 'server.js')
);
// Server 启动选项
const serverOptions: ServerOptions = {
run: {
module: serverModule,
transport: TransportKind.ipc
},
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: { execArgv: ['--inspect=6009'] }
}
};
// 客户端选项
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'dsl' }],
synchronize: {
configurationSection: 'dsl',
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.dsl')
}
};
// 创建并启动 Client
client = new LanguageClient(
'dslLanguageServer',
'DSL Language Server',
serverOptions,
clientOptions
);
client.start();
context.subscriptions.push(client);
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}第四步:Server 端完整实现
server/src/server.ts:
typescript
import {
createConnection,
TextDocuments,
ProposedFeatures,
InitializeResult,
TextDocumentSyncKind,
CompletionItem,
CompletionItemKind,
Diagnostic,
DiagnosticSeverity,
DocumentSymbol,
SymbolKind,
DocumentHighlight,
DocumentHighlightKind
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
const connection = createConnection(ProposedFeatures.all);
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// DSL 关键字
const KEYWORDS = ['entity', 'field', 'rule', 'when', 'then'];
// 关键字文档
const KEYWORD_DOCS: Record<string, string> = {
'entity': '定义数据实体。\n\n`entity User { ... }`',
'field': '为实体添加字段。\n\n`field name: string`',
'rule': '定义业务规则。\n\n`rule 名称 { when ... then ... }`',
'when': '规则触发条件。',
'then': '规则执行动作。'
};
// 配置
let maxLineLength = 100;
// 初始化
connection.onInitialize((): InitializeResult => {
return {
capabilities: {
textDocumentSync: {
openClose: true,
change: TextDocumentSyncKind.Incremental
},
completionProvider: { resolveProvider: true },
hoverProvider: true,
definitionProvider: true,
referencesProvider: true,
documentHighlightProvider: true,
documentSymbolProvider: true
},
serverInfo: {
name: 'dsl-language-server',
version: '0.1.0'
}
};
});
// 配置监听
connection.onDidChangeConfiguration((params) => {
const settings = params.settings;
if (settings?.dsl) {
maxLineLength = settings.dsl.maxLineLength ?? 100;
}
});
// 工具函数
function getLineText(
document: TextDocument,
line: number
): string {
return document.getText({
start: { line, character: 0 },
end: { line, character: Number.MAX_SAFE_INTEGER }
});
}
function getWordAtPosition(
document: TextDocument,
position: { line: number; character: number }
): string | undefined {
const lineText = getLineText(document, position.line);
const match = lineText.slice(0, position.character).match(/(\w+)$/);
return match ? match[1] : undefined;
}
function findAllOccurrences(
document: TextDocument,
word: string
): { line: number; character: number; length: number }[] {
const results: { line: number; character: number; length: number }[] = [];
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))) {
results.push({ line, character: match.index, length: word.length });
}
});
return results;
}
// 诊断验证
function validateDocument(document: TextDocument): void {
const diagnostics: Diagnostic[] = [];
const lines = document.getText().split('\n');
lines.forEach((lineText, line) => {
const trimmed = lineText.trim();
if (trimmed === '' || trimmed.startsWith('//')) return;
// 规则 1:未知关键字
const firstWord = trimmed.split(/\s+/)[0];
if (!KEYWORDS.includes(firstWord)) {
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'
});
}
// 规则 2:行过长
if (lineText.length > maxLineLength) {
diagnostics.push({
range: {
start: { line, character: maxLineLength },
end: { line, character: lineText.length }
},
message: `行过长(超过 ${maxLineLength} 字符)`,
severity: DiagnosticSeverity.Hint,
source: 'dsl'
});
}
// 规则 3:entity 未闭合
if (trimmed.startsWith('entity') && !trimmed.includes('{')) {
diagnostics.push({
range: {
start: { line, character: 0 },
end: { line, character: lineText.length }
},
message: 'entity 定义需要 {',
severity: DiagnosticSeverity.Error,
source: 'dsl'
});
}
});
connection.sendDiagnostics({ uri: document.uri, diagnostics });
}
// 补全
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 KEYWORDS
.filter((key) => key.startsWith(prefix))
.map((key) => ({
label: key,
kind: CompletionItemKind.Keyword,
detail: KEYWORD_DOCS[key]?.split('\n')[0] ?? 'DSL 关键字',
data: key
}));
});
// 补全详情
connection.onCompletionResolve((item) => {
const doc = KEYWORD_DOCS[item.data];
if (doc) {
item.documentation = { kind: 'markdown', value: doc };
}
return item;
});
// 悬停
connection.onHover((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return null;
const word = getWordAtPosition(document, params.position);
if (!word) return null;
const doc = KEYWORD_DOCS[word];
if (!doc) return null;
return {
contents: {
kind: 'markdown',
value: `### \`${word}\`\n\n${doc}`
}
};
});
// 定义跳转
connection.onDefinition((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return null;
const word = getWordAtPosition(document, params.position);
if (!word) return null;
// 查找定义(entity/field 声明)
const lines = document.getText().split('\n');
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 {
uri: document.uri,
range: {
start: { line, character: match.index },
end: { line, character: match.index + match[0].length }
}
};
}
}
return null;
});
// 引用查找
connection.onReferences((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return [];
const word = getWordAtPosition(document, params.position);
if (!word) return [];
return findAllOccurrences(document, word).map((occ) => ({
uri: document.uri,
range: {
start: { line: occ.line, character: occ.character },
end: { line: occ.line, character: occ.character + occ.length }
}
}));
});
// 文档高亮
connection.onDocumentHighlight((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) return [];
const word = getWordAtPosition(document, params.position);
if (!word) return [];
return findAllOccurrences(document, word).map((occ) => ({
range: {
start: { line: occ.line, character: occ.character },
end: { line: occ.line, character: occ.character + occ.length }
},
kind: DocumentHighlightKind.Read
}));
});
// 文档符号
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.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();第五步:语法高亮
syntaxes/dsl.tmLanguage.json:
json
{
"scopeName": "source.dsl",
"patterns": [
{
"name": "comment.line.double-slash.dsl",
"match": "//.*$"
},
{
"name": "keyword.control.dsl",
"match": "\\b(entity|field|rule|when|then)\\b"
},
{
"name": "entity.name.type.dsl",
"match": "\\b[A-Z]\\w*\\b"
},
{
"name": "string.quoted.double.dsl",
"match": "\"[^\"]*\""
},
{
"name": "constant.numeric.dsl",
"match": "\\b\\d+\\b"
}
]
}language-configuration.json:
json
{
"comments": {
"lineComment": "//"
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" }
]
}运行与验证
按 F5 启动调试:
| 步骤 | 操作 | 预期 |
|---|---|---|
| 1 | 新建 test.dsl 文件 | 语法高亮生效 |
| 2 | 输入 entity User { | 补全弹出关键字建议 |
| 3 | 输入未知词 | 黄色警告波浪线 |
| 4 | 悬停 entity | 显示关键字文档 |
| 5 | 光标停在 User 上 | 所有出现高亮 |
| 6 | 打开大纲 | 显示 entity/field 符号 |
功能扩展
| 扩展方向 | 实现 |
|---|---|
| 更完整语法 | 扩展 tmLanguage 与解析器 |
| 类型检查 | 字段类型校验 |
| 重构支持 | rename 实现 |
| 语义着色 | semanticTokens Provider |
| 代码片段 | Snippet 补全模板 |
常见问题
| 问题 | 处理 |
|---|---|
| 无语言能力 | 检查 documentSelector 与语言 ID |
| 高亮不生效 | 检查 tmLanguage scopeName |
| Server 崩溃 | 查看 Output 面板日志 |
| 补全无详情 | 确认 resolveProvider 与 data 字段 |
本实战完整搭建了 LSP Client + Server 全流程,覆盖语法高亮、诊断、补全、悬停、跳转、引用、高亮、符号八大能力,是「自定义语言支持插件」的完整模板。