找回密码
 立即注册
搜索

技术分享 如何实现 Json 文件的语法提示

0
回复
1253
查看
[复制链接]

465

主题

760

帖子

6450

积分

 楼主| 2021-7-13 15:55:40 显示全部楼层 |阅读模式

如何实现 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 文档


官方客服微信:kuaiyingyongguanKF
官方QQ群2:1012199894
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册