实战:JSON 增强插件
把语言服务特性综合运用,实现一个 JSON 增强插件:语法校验、智能补全、路径导航 CodeLens、值格式化,全面提升 JSON 编辑体验。
目标
- JSON 语法校验:解析错误实时提示
- 智能补全:键名与常用值建议
- 悬停文档:悬停键显示说明
- CodeLens:显示 JSON 路径导航
- 折叠:对象/数组区域折叠
第一步:注册全部 Provider
src/extension.ts:
typescript
import * as vscode from 'vscode';
import { JsonDiagnosticProvider } from './diagnostics';
import { JsonCompletionProvider } from './completion';
import { JsonHoverProvider } from './hover';
import { JsonCodeLensProvider } from './codelens';
import { JsonFoldingProvider } from './folding';
export function activate(context: vscode.ExtensionContext) {
// 诊断
const diagnostics = vscode.languages.createDiagnosticCollection('json-enhancer');
const diagnosticProvider = new JsonDiagnosticProvider(diagnostics);
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((doc) =>
diagnosticProvider.validate(doc)
),
vscode.workspace.onDidChangeTextDocument((event) =>
diagnosticProvider.validate(event.document)
),
vscode.workspace.onDidCloseTextDocument((doc) =>
diagnostics.delete(doc.uri)
),
diagnostics
);
// 补全
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
'json',
new JsonCompletionProvider(),
':', '"'
)
);
// 悬停
context.subscriptions.push(
vscode.languages.registerHoverProvider(
'json',
new JsonHoverProvider()
)
);
// CodeLens
const codeLensProvider = new JsonCodeLensProvider();
context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
'json',
codeLensProvider
)
);
// 折叠
context.subscriptions.push(
vscode.languages.registerFoldingRangeProvider(
'json',
new JsonFoldingProvider()
)
);
}第二步:诊断验证
src/diagnostics.ts:
typescript
import * as vscode from 'vscode';
export class JsonDiagnosticProvider {
constructor(
private readonly collection: vscode.DiagnosticCollection
) {}
validate(document: vscode.TextDocument): void {
if (document.languageId !== 'json') {
return;
}
const text = document.getText();
const diagnostics: vscode.Diagnostic[] = [];
// 尝试解析 JSON
try {
JSON.parse(text);
} catch (error) {
if (error instanceof Error) {
// 从错误消息提取位置
const positionMatch = error.message.match(/position (\d+)/);
const offset = positionMatch
? parseInt(positionMatch[1])
: 0;
const position = document.positionAt(offset);
const lineText = document.lineAt(position.line).text;
diagnostics.push(
new vscode.Diagnostic(
new vscode.Range(
position.line,
Math.max(0, position.character - 5),
position.line,
lineText.length
),
`JSON 解析错误: ${error.message}`,
vscode.DiagnosticSeverity.Error
)
);
this.collection.set(document.uri, diagnostics);
return;
}
}
// 结构检查:键名规范
const keyRegex = /"([^"]{2,})"\s*:/g;
let match: RegExpExecArray | null;
while ((match = keyRegex.exec(text))) {
const key = match[1];
if (key.includes(' ') || key.includes('-')) {
const start = document.positionAt(match.index + 1);
const end = document.positionAt(match.index + 1 + key.length);
diagnostics.push(
new vscode.Diagnostic(
new vscode.Range(start, end),
'键名建议使用驼峰命名',
vscode.DiagnosticSeverity.Hint
)
);
}
}
this.collection.set(document.uri, diagnostics);
}
}第三步:自动补全
src/completion.ts:
typescript
import * as vscode from 'vscode';
// 常见 JSON 键模板
const KEY_TEMPLATES: Record<string, string[]> = {
'': ['name', 'id', 'type', 'version', 'description'],
'config': ['debug', 'port', 'host', 'timeout'],
'server': ['host', 'port', 'protocol', 'cert'],
'database': ['host', 'port', 'user', 'password', 'name']
};
export class JsonCompletionProvider
implements vscode.CompletionItemProvider {
provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
): vscode.CompletionItem[] {
// 获取当前上下文(上一键名)
const context = this.getContextKey(document, position);
const templates = KEY_TEMPLATES[context] ?? KEY_TEMPLATES[''];
const items: vscode.CompletionItem[] = [];
templates.forEach((key) => {
const item = new vscode.CompletionItem(key);
item.kind = vscode.CompletionItemKind.Property;
item.detail = context ? `${context}.${key}` : '顶层键';
item.insertText = new vscode.SnippetString(
`"${key}": ${1:""}`
);
item.documentation = new vscode.MarkdownString(
`**${key}**\n\n${context ? context + '.' + key : key} 配置项`
);
item.sortText = '0';
items.push(item);
});
// 常用值补全
items.push(this.createValueItem('true', '布尔值'));
items.push(this.createValueItem('false', '布尔值'));
items.push(this.createValueItem('null', '空值'));
return items;
}
private createValueItem(value: string, detail: string): vscode.CompletionItem {
const item = new vscode.CompletionItem(value);
item.kind = vscode.CompletionItemKind.Value;
item.detail = detail;
return item;
}
private getContextKey(
document: vscode.TextDocument,
position: vscode.Position
): string {
// 向前查找最近的对象键
const text = document.getText(
new vscode.Range(
new vscode.Position(0, 0),
position
)
);
const matches = [...text.matchAll(/"(\w+)"\s*:\s*\{/g)];
return matches.length > 0
? matches[matches.length - 1][1]
: '';
}
}第四步:悬停文档
src/hover.ts:
typescript
import * as vscode from 'vscode';
// 键说明表
const KEY_DOCS: Record<string, string> = {
'debug': '是否开启调试模式。\n\n- true:输出详细日志\n- false:仅输出错误',
'port': '服务监听端口。\n\n默认值:`8080`',
'host': '服务绑定的主机地址。\n\n默认值:`0.0.0.0`',
'timeout': '请求超时时间(毫秒)。\n\n默认值:`30000`'
};
export class JsonHoverProvider implements vscode.HoverProvider {
provideHover(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Hover | undefined {
// 悬停在键名位置
const line = document.lineAt(position.line);
const keyMatch = line.text.match(/"(\w+)"\s*:/);
if (!keyMatch) {
return undefined;
}
const keyStart = line.text.indexOf(keyMatch[1]);
const keyEnd = keyStart + keyMatch[1].length;
// 只对键名范围内的悬停响应
if (position.character < keyStart || position.character > keyEnd) {
return undefined;
}
const key = keyMatch[1];
const doc = KEY_DOCS[key];
if (!doc) {
return undefined;
}
const contents = new vscode.MarkdownString();
contents.appendMarkdown(`### \`${key}\`\n\n`);
contents.appendMarkdown(doc);
return new vscode.Hover(
contents,
new vscode.Range(position.line, keyStart, position.line, keyEnd)
);
}
}第五步:CodeLens 路径导航
src/codelens.ts:
typescript
import * as vscode from 'vscode';
export class JsonCodeLensProvider implements vscode.CodeLensProvider {
private _onDidChangeCodeLenses =
new vscode.EventEmitter<void>();
readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event;
provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] {
const lenses: vscode.CodeLens[] = [];
for (let line = 0; line < document.lineCount; line++) {
const text = document.lineAt(line).text;
// 匹配对象键
const match = text.match(/^\s*"(\w+)"\s*:\s*\{/);
if (match) {
const key = match[1];
const indent = text.length - text.trimStart().length;
lenses.push(
new vscode.CodeLens(
new vscode.Range(line, 0, line, text.length),
{
command: 'jsonEnhancer.navigate',
title: `${key} (对象)`,
arguments: [document, line, key]
}
)
);
}
}
return lenses;
}
}第六步:折叠 Provider
src/folding.ts:
typescript
import * as vscode from 'vscode';
export class JsonFoldingProvider
implements vscode.FoldingRangeProvider {
provideFoldingRanges(
document: vscode.TextDocument
): vscode.FoldingRange[] {
const ranges: vscode.FoldingRange[] = [];
const stack: { line: number }[] = [];
for (let line = 0; line < document.lineCount; line++) {
const text = document.lineAt(line).text;
for (const char of text) {
if (char === '{' || char === '[') {
stack.push({ line });
} else if (char === '}' || char === ']') {
const open = stack.pop();
if (open && open.line !== line) {
ranges.push(
new vscode.FoldingRange(
open.line,
line,
vscode.FoldingRangeKind.Region,
`... ${line - open.line} 行`
)
);
}
}
}
}
return ranges;
}
}第七步:导航命令
src/extension.ts 追加:
typescript
// 路径导航命令
context.subscriptions.push(
vscode.commands.registerCommand(
'jsonEnhancer.navigate',
(document: vscode.TextDocument, line: number, key: string) => {
vscode.window.showInformationMessage(
`当前位置: ${key}(第 ${line + 1} 行)`
);
// 在状态栏显示路径
vscode.window.setStatusBarMessage(
`$(symbol-namespace) JSON 路径: ${key}`,
3000
);
}
)
);运行与验证
按 F5 启动调试:
| 步骤 | 操作 | 预期 |
|---|---|---|
| 1 | 打开 .json 文件 | 对象/数组可折叠 |
| 2 | 输入错误 JSON | 红色波浪线 + 问题面板 |
| 3 | 输入 " 触发补全 | 弹出键名建议 |
| 4 | 悬停 port 键 | 显示配置说明 |
| 5 | 查看对象行上方 | CodeLens 显示键名 |
| 6 | 点击 CodeLens | 状态栏显示路径 |
功能扩展
| 扩展方向 | 实现 |
|---|---|
| JSON Schema 校验 | 集成 schema 库 |
| 值类型检查 | 按 schema 校验类型 |
| 格式化 | 提供文档格式化 |
| 大文件优化 | 防抖 + 增量诊断 |
| 跳转定义 | 引用关联跳转 |
常见问题
| 问题 | 处理 |
|---|---|
| 诊断误报 | 收紧解析错误位置计算 |
| 补全不触发 | 检查触发字符 |
| CodeLens 不刷新 | 触发 onDidChangeCodeLenses |
| 折叠错位 | 精确维护括号栈 |
本实战综合了诊断、补全、悬停、CodeLens、折叠五大语言服务,是「编辑增强类」插件的完整模板。