如何实现 Json 文件的语法提示
快应用使用 manifest.json
配置应用的基本信息。manifest.json 的属性字段多,配置时经常需要查阅官方文档。所以,希望实现语法提示功能,包括自动补全和 hover 查看属性信息,减少开发者查阅文档次数。本篇文章,就“如何实现 Json 文件的语法提示”,跟大家分享下。
manifest.json 增加语法提示
-
最终效果
我们先看看最终效果:
如图所示,会自动提示属性和属性值,选中后可自动补全。
-
新建插件
快应用开发工具是基于 vscode 开发的,支持加载 vscode 插件。vscode 插件,可以通过配置 jsonValidation 字段,实现 json 文件的智能提示功能。首先,我们需要新建插件。如何新建插件,请参考 Your First Extension。
-
配置 package.json
新建插件后,按文档配置 package.json 即可。具体如下:{ "contributes": { "jsonValidation": [ { "fileMatch": "manifest.json", "url": "./schemas-manifest.json" } ] } }
fileMatch 用于指定匹配的文件,除了填写完整的文件名,也支持模糊匹配,比如
*icon-theme.json
。
url 用于指定 jsonSchemas 文件,支持本地路径,也支持远程服务的 url,例如https://json.schemastore.org/jshintrc
,常用的 shcemas 可以从 JSON Schema Store 获取。 -
编写 schemas-manifest.json
schemas-manifest.json 的一部分配置如下:{ // 定义必填字段 "required": [ "package", "name", "icon", "versionCode", "permissions", "config", "router" ], // 属性值 "properties": { // package 字段:类型为 string,description 为该字段的描述信息,当鼠标放在该字段上可以查看描述信息。 "package": { "type": "string", "description": "应用包名,确认与原生应用的包名不一致,推荐采用 com.company.module 的格式,如:com.example.demo" }, // 支持多层嵌套,比如 router.pages,router.page.${pagePath} "router": { "type": "object", "description": "路由信息", "properties": { "pages": { "type": "object", "description": "页面配置列表,key 值为页面名称(对应页面目录名,例如 Hello 对应'Hello'目录),value 为页面详细配置 page", // pages 字段,是 object 类型,但 key 值是不确定的页面名称,所以不是设置 properties 字段,而是设置 patternProperties,对 key 值进行正则匹配。 "patternProperties": { ".+": { "type": "object", "required": ["component"], "properties": { "component": { "type": "string", "description": "页面对应的组件名,与 ux 文件名保持一致,例如'hello' 对应 'hello.ux'" } // 略:其他属性 } } } } // 略:其他属性 } } } }
更多 json schemas 的配置方法,可以查看其官方文档
开发者自定义语法提示
除了开发插件,IDE 的用户也可以修改 IDE 中的配置,来增加 json 的语法提示。具体操作如下:
- 打开 setting 面板
- 搜索 json.schemas
- 增加自定义配置,配置方法和
jsonValidation
相同,而 custom-schema.json 的编写方法,也和 schemas-manifest.json 相同。"json.schemas": [ { "fileMatch": ["custom.json"], "url": "./custom-schema.json" } ]
json schemas 是什么?
JSON Schema 本身是用 JSON 编写的,用于描述其他 JSON 文件的数据结构。
优点:跨语言,编写简单
缺点:不能对字段之间的关系进行限制,所以无法实现一些复杂的校验需求
vscode 如何整合 json schemas?
vscode 内置语法插件 json-language-features
,用于提供 json 文件的语法提示。该语法插件分为 client
和 server
,两者通过 language server protocol 通信。由 server
解析文件并生成提示信息,发送给 client
显示。
client
端会遍历插件的 package.json
的 contributes.jsonValidation
字段,并发送给 server
。
client.onReady().then(() => {
// client 发送消息
client.sendNotification(
SchemaAssociationNotification.type,
getSchemaAssociations(context)
);
});
// 获取 schema 配置
function getSchemaAssociations(
_context: ExtensionContext
): ISchemaAssociation[] {
const associations: ISchemaAssociation[] = [];
extensions.all.forEach((extension) => {
const packageJSON = extension.packageJSON;
if (
packageJSON &&
packageJSON.contributes &&
packageJSON.contributes.jsonValidation
) {
const jsonValidation = packageJSON.contributes.jsonValidation;
if (Array.isArray(jsonValidation)) {
jsonValidation.forEach((jv) => {
let { fileMatch, url } = jv;
if (typeof fileMatch === "string") {
fileMatch = [fileMatch];
}
if (Array.isArray(fileMatch) && typeof url === "string") {
let uri: string = url;
if (uri[0] === "." && uri[1] === "/") {
uri = joinPath(extension.extensionUri, uri).toString();
}
fileMatch = fileMatch.map((fm) => {
if (fm[0] === "%") {
fm = fm.replace(/%APP_SETTINGS_HOME%/, "/User");
fm = fm.replace(/%MACHINE_SETTINGS_HOME%/, "/Machine");
fm = fm.replace(/%APP_WORKSPACES_HOME%/, "/Workspaces");
} else if (!fm.match(/^(\w+:\/\/|\/|!)/)) {
fm = "/" + fm;
}
return fm;
});
associations.push({ fileMatch, uri });
}
});
}
}
});
return associations;
}
server
接收到消息后,会执行 updateConfiguration()
更新配置,调用 languageService.configure(languageSettings)
,更新 languageService
中的 schema
配置。
// The jsonValidation extension configuration has changed
// 接收到消息
connection.onNotification(SchemaAssociationNotification.type, associations => {
schemaAssociations = associations;
updateConfiguration();
});
function updateConfiguration() {
const languageSettings = {
validate: true,
allowComments: true,
schemas: new Array<SchemaConfiguration>()
};
if (schemaAssociations) {
if (Array.isArray(schemaAssociations)) {
Array.prototype.push.apply(languageSettings.schemas, schemaAssociations);
} else {
for (const pattern in schemaAssociations) {
const association = schemaAssociations[pattern];
if (Array.isArray(association)) {
association.forEach(uri => {
languageSettings.schemas.push({ uri, fileMatch: [pattern] });
});
}
}
}
}
if (jsonConfigurationSettings) {
jsonConfigurationSettings.forEach((schema, index) => {
let uri = schema.url;
if (!uri && schema.schema) {
uri = schema.schema.id || `vscode://schemas/custom/${index}`;
}
if (uri) {
languageSettings.schemas.push({ uri, fileMatch: schema.fileMatch, schema: schema.schema });
}
});
}
// languageService 更新配置
languageService.configure(languageSettings);
// Revalidate any open text documents
documents.all().forEach(triggerValidation);
}
languageService
对应 vscode-json-languageservice,它提供了设置 shcema、校验、自动补全、hover 提示等接口给 server
调用。其具体实现比较繁琐,这里不再深入探究,有兴趣的可以阅读其源码。
export function getLanguageService(
params: LanguageServiceParams
): LanguageService {
const promise = params.promiseConstructor || Promise;
const jsonSchemaService = new JSONSchemaService(
params.schemaRequestService,
params.workspaceContext,
promise
);
jsonSchemaService.setSchemaContributions(schemaContributions);
const jsonCompletion = new JSONCompletion(
jsonSchemaService,
params.contributions,
promise,
params.clientCapabilities
);
const jsonHover = new JSONHover(
jsonSchemaService,
params.contributions,
promise
);
const jsonDocumentSymbols = new JSONDocumentSymbols(jsonSchemaService);
const jsonValidation = new JSONValidation(jsonSchemaService, promise);
return {
// 设置 schema
configure: (settings: LanguageSettings) => {
jsonSchemaService.clearExternalSchemas();
if (settings.schemas) {
settings.schemas.forEach((settings) => {
// 注册外部 shemas
jsonSchemaService.registerExternalSchema(
settings.uri,
settings.fileMatch,
settings.schema
);
});
}
jsonValidation.configure(settings);
},
resetSchema: (uri: string) => jsonSchemaService.onResourceChange(uri),
// 校验
doValidation: jsonValidation.doValidation.bind(jsonValidation),
parseJSONDocument: (document: TextDocument) =>
parseJSON(document, { collectComments: true }),
newJSONDocument: (root: ASTNode, diagnostics: Diagnostic[]) =>
newJSONDocument(root, diagnostics),
getMatchingSchemas: jsonSchemaService.getMatchingSchemas.bind(
jsonSchemaService
),
doResolve: jsonCompletion.doResolve.bind(jsonCompletion),
// 补全
doComplete: jsonCompletion.doComplete.bind(jsonCompletion),
findDocumentSymbols: jsonDocumentSymbols.findDocumentSymbols.bind(
jsonDocumentSymbols
),
findDocumentSymbols2: jsonDocumentSymbols.findDocumentSymbols2.bind(
jsonDocumentSymbols
),
findDocumentColors: jsonDocumentSymbols.findDocumentColors.bind(
jsonDocumentSymbols
),
getColorPresentations: jsonDocumentSymbols.getColorPresentations.bind(
jsonDocumentSymbols
),
// hover 提示
doHover: jsonHover.doHover.bind(jsonHover),
getFoldingRanges,
getSelectionRanges,
findDefinition: () => Promise.resolve([]),
findLinks,
format: (d, r, o) => {
let range: JSONCRange | undefined = undefined;
if (r) {
const offset = d.offsetAt(r.start);
const length = d.offsetAt(r.end) - offset;
range = { offset, length };
}
const options = {
tabSize: o ? o.tabSize : 4,
insertSpaces: o?.insertSpaces === true,
insertFinalNewline: o?.insertFinalNewline === true,
eol: "\n",
};
return formatJSON(d.getText(), range, options).map((e) => {
return TextEdit.replace(
Range.create(
d.positionAt(e.offset),
d.positionAt(e.offset + e.length)
),
e.content
);
});
},
};
}
tip
语法插件,经常分为 client 和 server 两端,主要是基于以下考虑:
- 适配:server 端负责语法分析,只要求按照语法服务协议与客户端通信,可以用任何语言实现。一套代码,可以适配不同的 IDE。
- 性能:语法分析通常会占用大量 CPU 和内存,在单独的进程中运行可以降低 IDE 的性能成本。语法分析出错时,也不会影响 IDE 的插件进程。
具体如何开发语法插件,可以查看vscode 文档。