如何实现 Json 文件的语法提示

快应用开发工具 Jul 5, 2021

快应用使用 manifest.json 配置应用的基本信息。manifest.json 的属性字段多,配置时经常需要查阅官方文档。所以,希望实现语法提示功能,包括自动补全和 hover 查看属性信息,减少开发者查阅文档次数。本篇文章,就“如何实现 Json 文件的语法提示”,跟大家分享下。

manifest.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 文件的语法提示。该语法插件分为 clientserver,两者通过 language server protocol 通信。由 server 解析文件并生成提示信息,发送给 client 显示。

client 端会遍历插件的 package.jsoncontributes.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 文档

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.