从 TypeScript 到 VSCode —— 语言服务插件导引

1,187 阅读6分钟

前言

众所周知,在如今的开发者中 TypeScript 日渐流行。TypeScript 提供了强劲的类型检查能力,除此之外,比较不为人知的是,它还以 language service 的形式提供了针对 TypeScript 和 JavaScript 开发体验的编辑器支持能力,例如重构,快速修复,自动补全等。并且 TypeScript 提供了基础的针对 language service 的扩展能力。
VSCode 也是当下非常流行的一款 editor。VSCode 提供了丰富的扩展能力,同时, VSCode 作为同属 Microsoft 维护的开源项目,也与 TypeScript 进行了深度的集成和整合。

本文以作者的一款 TypeScript/VSCode 插件 ts-string-literal-enum-plugin (将 Enum 转换为字符串字面量 Enum的插件) 为线索,进行了对 TypeScript/VSCode/Language Service 生态的一次探索。

背景

在 TypeScript 众多的 issue 中,有一个 (#16464) 希望可以扩展 Enum 语法的提案,这个提案的目的在于提供一种简便方法,以允许大家快捷方便的定义 “字符串字面量枚举”,即如下形式:

enum Action {
   LOAD_PROFILE = "LOAD_PROFILE",
   ADD_TASK = "ADD_TASK",
   REMOVE_TASK = "REMOVE_TASK"
}

对于经常会用到字符串字面量枚举的开发者来说,可能需要很多恼人的 “复制-粘贴” 或多行编辑操作,而这会降低开发者的体验。因此提案提议支持如下语法,当做上面定义的语法糖:

enum Action: string {
   LOAD_PROFILE,
   ADD_TASK,
   REMOVE_TASK
}

作者对此持支持的观点,但由于种种原因,TypeScript 团队目前并没有接受这个提案,并且提出了一些解决方案,例如提供重构动作以允许用户方便的转换 Enum 的形式。
而作者则实现了 TypeScript 团队所提出的解决方案,并与大家分享相关的经验。

实现

TypeScript language service plugin

TypeScript 提供了对于 language service plugin 的支持,允许用户扩展 language service 的能力,并且有相关文档,本文不再赘述。

API

首先,我们需要扩展 TypeScript language service 提供的功能。而我们首先要知道 language service 提供了哪些功能,以及这些功能对外暴露出的接口定义,这些信息可以在 TypeScript 类型定义中 找到。由于我们的目的是增加重构动作,根据上方提到的类型定义可知,我们需要扩展 language service 中的 getApplicableRefactorsgetEditsForRefactor 两个接口。

顾名思义,getApplicableRefactors 代表根据提供的信息,获取可以在当前上下文使用的重构动作。而 getEditsForRefactor 则是在当前上下文中,获取使用重构所需的文本编辑操作。

Plugin

简单来说,TypeScript 期望得到并加载 export = (mod: { typescript: ts }) => { create(info: ts.server.PluginCreateInfo): ts.LanguageService } 形式的 plugin,即 TypeScript 会将运行时使用的 TypeScript 与一些运行时信息 (例如当前所使用的 language service 实例) 注入 plugin 中。
值得注意的是,我们需要使用 mod.typescript - 即被注入至 plugin 中的 TypeScript 库来作为我们 plugin 中使用到的 TypeScript 库,这样做有两个原因:

  • 不同版本的 TypeScript 可能有很多名字相同但值不同的定义,若不使用被注入 plugin 的 TypeScript 库可能会造成兼容性问题。
  • 可以减少打包 plugin 时对 TypeScript 的依赖,减小 plugin 体积。

我们的目的是扩展原有的 language service,也就是说需要保留原有的功能,在此基础上增加我们自定义的操作,即类似 decoration/proxy 的行为。

实现 plugin 所需的基础代码不再赘述,可以在 src/plugin.ts, src/decorator.tssrc/index.ts 中查看。

Service

Service 也是 plugin 的核心功能由, 即提供可用重构动作和与之对应的文本编辑操作。、

由上文提到的类型定义中可知,getApplicableRefactorsfileName, positionOrRange 等参数,其中我们只关注这 2 个参数。getEditsForRefactor 则有 fileName, positionOrRange, refactor, actionName 等参数,其中我们只关注这 4 个参数。

整体实现有很多细节,本文不再赘述,仅简述主要逻辑:

  • 实现 getApplicableRefactors
    1. 根据 fileNamepositionOrRange 获取触发重构动作的位置与与之对应的 AST Node。
    2. 判断当前 AST Node 是否符合我们的预期。 plugin 中提供了 将 Enum 或 Enum member 转换为“字符串字面量枚举” 两个功能,也就是判断是 Node 否为 EnumDeclarationEnumMember,并且若为 EnumMember,则不能有初始化表达式(称之为 可转换的);若为 EnumDeclaration, 则至少有一个 member 为可转换的
    3. 若符合预期,则根据当前节点信息返回对应的重构动作信息,用于在 VSCode 中展示。
  • 实现 getEditsForRefactor
    1. 根据 fileNamepositionOrRange 获取要使用重构动作的位置与与之对应的 AST Node。
    2. 进行语法转换,为 Enum member 生成初始化表达式。其中使用到的 TextChanges APITypeScript Internal API 可以在 一种编辑 TypeScript 代码的方式open-typescript 中找到。
    3. 返回要使用的重构动作对应的文本操作。
构建与使用

package.json 中指定 maintsc build 指定的目录后,即可以 TypeScript language service plugin 的形式测试与使用。 我们可以以 file:yarn pack 的形式将其安装到项目中,在 tsconfig.json 中指定 { "compilerOptions": { "plugins": [{ "name": "《plugin package》" }] }},并在 VSCode 中选择使用 node_modules 中的 TypeScript 即可。

VSCode extensions

上文提到,VSCode 与 TypeScript 有深度的集成和整合(TypeScript Extensions 也由 VSCode 团队在维护),拜此所赐,我们可以非常方便快捷的将一个 TypeScript plugin 集成为 VSCode extensions。

Contribution Points

如果你熟悉 VSCode 开发,那你一定知道 VSCode 有许多 Contribution Points,我们可以通过这些 Contribution Points 来一定程度上影响到 VSCode 的行为。

而幸运的是,VSCode 提供了 typescriptServerPlugins 这样一个 contribution point。我们可以利用这个 contribution point 来直接将 TypeScript language service plugin 加载到 VSCode 中。

实现

对于我们实现的 plugin 来说,目前并不需要一些额外的交互(例如:VSCode 中的配置项等),因此我们只需要在 package.json 中补充需要的信息(如 nameicon 等)即可完成 extension 的开发。可以在这里看到完整配置

构建与发布则可以参考官方文档: publishing-extension,为了更好的体验,我们还可以指定 vscodeignore 以忽略不必要的文件,增加 README 等。在本地构建出 .vsix 文件后,便可在 VSCode 中安装和使用 extension。

自动化

我们还可以实现对于 PR 进行 gated build, 或对于 release 自动 publish 到 npm 以及 vscode marketplace。这部分与其他项目大同小异,不再赘述。

结语

TypeScript 和 VSCode 都是当前最火热最流行的项目之一,本文由一个 TypeScript issue 出发,将 TypeScript,TypeScript plugin 及 VSCode extension 串联起来。揭开了 TypeScript/VSCode 生态的冰山一角。希望可以帮助到大家。

感谢阅读。