VSCode 插件 - YAI
介绍
这又是一次目标回收计划,早在 2021 年我还在广泛地写 TypeScript 代码时就想完成这样一个插件来满足我“不打断心流地引入模块”的需求,但“新建文件夹”之后我一直没有实际的迭代动作。直到最近高频写 Go 代码时,才真正意识到这个需求的重要性。于是我又重新打开了这个项目的代码仓库。
这是一个 VSCode 插件,叫做 YAI,全称 Yet Another Importer,英文项目命名的 Yet Another 数不胜数,我也随波逐流一次。这个插件是用来帮助开发者方便地引入模块的,它可以自动识别当前项目中的依赖,并且扫描项目本地的文件,统计当前项目中引入模块的规律和频次,在需要引入模块时给出相应提示,并且以编程语言“原生”的方式将模块引入到代码中。何为“原生”,也就是适应当前项目编程语言的引入方式,比如在 JavaScript 项目中,它会使用 import
或 require
语句引入模块,而在 Python 项目中,它会使用 import
语句引入模块,在 Go 项目中,它会使用别名来引入模块等。
目前这个插件还处于开发阶段,但是已经可以在 Go 语言中使用了。目前规划的编程语言还有 ECMAScript、Python、C/C++ 这几种。该插件的代码仓库在 Github 代码仓库中,如果你也对这个插件感兴趣,欢迎使用或者参与开发。
插件已发布,可以在 yai - VSCode Marketplace 查看安装。
这篇文章可以算作插件的设计文档,我会在这里记录一些关于这个插件的设计思路和实现细节。
开发初衷
从代码仓库的提交日期可以看到,我三年前就想实现这样的小插件,当时主要编程语言是 TypeScript,主要写 React 项目,虽然 VSCode 对 TypeScript 的模块引入支持得还算不错,但有时它也会不及预期:
- 在实现的具体代码逻辑中键入一个模块名,有时 VSCode 能够自动提示,有时则不能,例如引入
React
; - 在已经部分引入模块的情况下,再想引入模块中其他导出变量,有时 VSCode 能够自动提示,有时则不能(大多时候不能),例如已经从
antd
中引入了Button
,Tooltip
,message
再想引入Select
时,往往无法得到编辑器的提示; - 在引入本地定义的模块时,VSCode 有时无法正确判断导出方式,
export default
和export
有时会混淆。
为了解决上述 VSCode 模块引入的一些问题,我需要暂停手头的工作,把视窗划到文件的头部(通常情况下可以使用 Cmd
+ Up Arrow
组合键来完成),然后键入 import
或 require
等关键字,再键入模块名,最后再键入可选的分号。这样的操作虽然看似简单,但是在频繁引入模块的情况下,会打断我的心流,让我无法专注于当前的工作。
这个情况在写 Go 代码的时候也时常出现,虽然 Go 语言的模块引入方式相对简单,但是在引入第三方模块时,我还是需要打开浏览器,查找模块的文档,然后复制粘贴模块的引入语句。而且在 Go 代码中会广泛地使用别名来引入模块,若一个项目 Go 文件稍微多些,同一个模块会出现多个引入别名(例如 VSCode 有时将 k8s.io/apimachinery/pkg/apis/meta/v1
和 k8s.io/api/core/v1
都引入为 v1
,有时又分别引入为 metav1
和 corev1
),为了保持各个文件中该模块的语义和含义一致,我不仅需要跳转到文件头部,甚至需要打开其他文件将别名引入语句复制粘贴到当前文件中。这种操作无疑是对“流畅编写代码”目标的一次沉重打击。
设计思路和实现细节
根据开发初衷,我给这个插件制定了如下几个目标,优先级从高到底:
- 模块引入:需要提供不打断心流的引入模块方式,即在键入模块名时,插件应该能够自动提示当前项目中的可引入的模块;
- 模块索引:需要支持本地文件中引入模块的索引,即插件应该能够扫描当前项目中的文件,统计当前项目中引入模块的规律和频次;
- 多语言支持:需要支持多种编程语言,即插件应该能够根据当前项目的编程语言,使用该编程语言的原生引入方式引入模块;
为了实现这三个目标,结合近期编写 Go 代码的经历,我给 YAI 插件制定了如下设计思路。
模块引入
模块引入是插件的核心功能,结合我的日常使用习惯,开发者通常会引入的模块有三种:
- 标准库模块:Go 语言的标准库模块,例如
fmt
,os
,io
等; - 第三方模块:Go 语言的第三方模块,例如
github.com/gin-gonic/gin
,github.com/spf13/viper
等; - 本地模块:项目中的本地模块,例如
pkg/utils
,internal/config
等。
为了识别这三个类型的模块,插件需要扫描当前项目中的 go.mod
文件,以识别第三方模块;扫描当前项目中的 go
文件,以识别本地模块;最后至于标准库模块,Go 语言的标准库模块是有限的,插件可以直接内置这些模块。例如执行 go list std
命令就可以把所有的标准库都列出来。
秉持着先能跑再优化的开发理念,该模块引入目标的迭代计划是:
- 第一步:所有的模块引入都需要开发者提供完整的模块名,例如
github.com/gin-gonic/gin
,pkg/utils
等,不提供补全提示; - 第二步:在键入模块名时,插件应该能够自动提示当前项目中的可引入的模块,例如在 Go 项目中,键入
gin
时,插件应该能够提示github.com/gin-gonic/gin
; - 第三步:在键入模块名时,插件应该能够自动提示当前项目中的可引入的模块,同时在键入
.
时,插件应该能够自动提示当前模块的导出变量。
模块索引
模块索引需要扫描本地所有源文件,解析其中的模块引入语句,统计当前项目中引入模块的规律和频次。这个功能的目的是为了在模块引入时,给出更加智能的提示。例如在 Go 项目中对于第三方模块而言,有时开发者会引入模块的子模块,例如 github.com/gin-gonic/gin
中的 github.com/gin-gonic/gin/render
,有时开发者会引入模块的别名,例如 github.com/gin-gonic/gin
有时会引入为 gin
,有时会引入为 g
,有时会引入为 g1
等。这些引入方式都是合法的,但是在一个项目中应该保持一致,这样可以提高代码的可读性和可维护性。
模块索引是为了能在模块引入时提供更多的信息,以更好地满足模块引入的需求。在 YAI Go 的第一个版本中,模块索引所实现的功能就是把本地文件中所有的引入路径都扫描出来,然后在模块引入时给出提示,这很好地补充了仅扫描 go.mod
文件获取第三方模块根路径的不足。同时,Go 项目的模块索引可以能获取到模块的别名,在模块引入时能让开发者直观地看到这个模块的所有别名以及各个别名的引入频次。
多语言支持
最初的开发动力来自于 TypeScript 编写的 React 项目,但是在写 Go 项目的时候,我发现不打断心流地模块引入是一个更加普遍的需求。因此,我决定在 YAI 插件中支持多种编程语言,因为当前接触得比较多的是 Go 语言和 ECMAScript 语言,所以我决定先支持这两种语言。
我定义了一个名为 InnerProcessor
的接口,其中提供了两个方法 index()
和 import()
,由各个语言的处理器分别实现该接口,例如 Go 语言的 GolangProcessor
。外层定义 LanguageProcessor
接口拓展了 InnerProcessor
额外提供获取当前项目编程语言的方法 getLanguageId()
,再定义类 YAIProcessor
实现这个接口,用于根据当前项目的编程语言选择合适的处理器。
每当激活插件后打开某个项目,若插件检测到当前项目的编程语言是已经支持的,则会触发一次 index()
方法的调用(YAI: Index Modules
)。而模块引入功能则需要用户主动触发,即通过快捷键或者命令面板调用执行,命令名称为 YAI: Import Module
。执行模块引入命令后会通过一系列输入选择的交互引入用户所指定的模块。
实现效果
下面是 YAI 插件在 Go 项目中实现功能的一些实现效果。
首先通过 YAI: Index Modules
命令来索引当前项目中的模块,索引建立成功之后会有提示:
打开命令面板(F1
或 Ctrl/Cmd + Shift + P
) 通过 YAI: Import Module
命令来开启引入模块功能。
进入命令后会展示当前可以引入的模块,这里可以通过键盘上下键选择模块,也可以通过输入文本进行筛选,然后回车确认选择:
选择模块后插件会提示用户是否需要引入该模块的子模块,此处如果不输入(直接按回车或 ESC
键)则表示不引入子模块:
下一步插件会提示本项目中该模块使用的所有的别名,用户可以选择其中一个别名,也可以手动再输入一个别名,然后回车确认选择:
最后插件会在当前文件中插入引入语句,模块引入成功:
总结和不足
YAI 的开发占据了我近期工作日的业余时间和周末一整天,不过这个插件的开发过程还是很有意思的。在开发过程中,我学到很多关于 VSCode 插件开发的知识,也大致明白了 VSCode Extension API 能够实现的功能。不得不说 VSCode 对插件开发的支持还是很好的,它提供了丰富的 API,让开发者可以很方便地实现自己的想法。
就 YAI 目前的功能而言,它已经能够满足我在 Go 项目中引入模块的需求,让我不打断心流地引入我指定的模块。但它是还有很多可以迭代的方向:
- 交互流程有些冗长,需要用户多次输入和选择,未来可以考虑通过配置项的方式优化交互流程,例如省略输入子模块的步骤等;
- 目前只支持 Go 语言,未来可以考虑支持更多的编程语言,例如 ECMAScript、Python、C/C++ 等;
- 可以针对每个语言推出特定的功能,例如统一 Go 项目中的模块别名等。