vlambda博客
学习文章列表

XCode 代码补全插件 - JSPatchX 原理解析

JSPatchXJSPatch Xcode 代码自动补全插件,目前在 github 开源。

做完一个开源项目照例写篇文章说明下实现原理,主要目的是让想对这个项目做贡献改进的人可以通过文章更容易地了解这个项目的由来,思路,核心原理和流程,降低参与这个项目开发的门槛。

由来

JSPatch 脚本一个不爽的地方就是没有代码补全,而调用 OC 方法时方法名又死长,写起来很不方便。

对此之前做了 JSPatch Convertor,可以自动把 OC 代码转为 JSPatch 脚本,这个工具的使用场景是用 JSPatch 做 hotfix 时,需要重写原 OC 的整个方法,这时用工具把这个方法的 OC 代码直接转为 JS 再进行修改,可以很大地降低工作量,缓解了这个问题。但若要用 JSPatch 开发新功能模块,就不会有 OC 代码可以去转换,这时提高编码效率的唯一方式就是做代码补全插件。

在寻找实现方案的时候得知公司内一牛人 louis 已经实现了 lua 的 XCode 代码补全插件,沟通后还很慷慨地给了源码,省去了很多研究 XCode 代码补全机制的功夫,于是参考他的编码,并且直接用了他 OC 头文件解析的代码,开发了 JSPatchX。所以这个项目算是我与 louis 联合开发的,在此感谢 louis~

插件入门

XCode 有个很坑爹的地方,就是它并不官方支持插件开发,官方没有文档,XCode 也没有开源,但由于 XCode 是 Objective-C 写的,OC 动态性太强大,导致在这么封闭的情况下民间还是可以做出各种插件,其核心开发方式就是:

  1. dump 出 Xcode 所有头文件,知道 Xcode 里有哪些类和接口。

  2. 通过头文件方法名猜测方法的作用,swizzle 这些方法,插入自己的代码实现插件逻辑。

  3. 通过 NSNotificationCenter 监听各种事件的发生。

更详细的开发教程网上有不少文章,有兴趣的自行搜索吧。

起步

对于实现 JS 代码补全这个功能来说,主要分三步:

  1. 在编辑 JS 文件时开启代码补全功能。

  2. 找到用户输入代码时的回调,按 Xcode 要求组装代码补全对象数组返回。

  3. 根据已输入文字对补全对象数组进行过滤

第一步是通过替换 DVTTextCompletionDataSource 类里的 -strategies 方法,在源文件是 JS 时生成一个 IDEIndexCompletionStrategy 对象返回,就可以针对 JS 文件走代码补全逻辑了。

第二步是替换 IDEIndexCompletionStrategy- completionItemsForDocumentLocation:context:highlyLikelyCompletionItems:areDefinitive: 方法,这个方法会在用户输入时被调用,在这里组装好应该出现的补全对象(IDEIndexCompletionItem) 列表返回,Xcode 就会自动应用返回的 items 对输入进行补全。

第三步是在 DVTTextCompletionSession-_setFilteringPrefix:forceFilter: 方法,针对第二步返回的 item 对象根据输入进行过滤。

要让自动补全插件程序跑通,只需实现上述三步。显然核心在第二步如何组装合适的补全对象 completionItem。代码里我们新增了一个 IDEIndexCompletionItem 的子类 JPCompletionItem 去表示,下面统一把这个补全对象称为 completionItem。接下来的问题就是怎样组装这些 completionItem。

实现

先看看我们需要哪些自动补全,概括起来有几种:

  1. 可能会被调用到的 OC 方法名

  2. JS 上新增的方法名,以及出现的类名

  3. JSPatch 自身的一些关键字接口,如 defineClass, require 等

  4. 当前 JS 文件里出现过的关键字

前三点应该没有异议,第四点要解释一下,实际上若要做得精细,应该加上 JS 语言本身自带的 API 和关键字(var / Math 函数 / String 函数等),以及JS 当前作用域上的变量的补全,但这样做一是 API 太多,二是实现复杂,所以用 “当前 JS 文件里出现过的关键字” 代替这两点,只要文件里出现过的单词就会有补全提示,也就是说一些关键字和变量第一次输入时没有提示,但在同个文件第二次输入就有补全提示了,sublime 默认就是这样的补全规则,实际使用效果很好,所以选择用这种简单的方式满足需求。

具体实现上,分三步走,一是解析 OC 头文件,二是解析 JS 文件,三是对解析后的数据进行缓存和组装 completionItem。

解析 OC 头文件

JPObjcFile 负责解析 OC 头文件,因为这里可以认为外部可以调用的 OC 接口都在头文件里,所以只需要解析头文件,这样处理比较简单,解析效率也很高。louis写了个 OC 头文件解析器,把头文件里的 class / protocol / import 解析出来,最终每个头文件都会解析成对应的 JPObjcFile 对象,这个对象保存着文件里 class / protocol 对应的方法的 completionItems,可以按需求直接输出。

解析 JS 文件

JPJSFile 负责解析 JS 文件,这里的解析比较简单,没有用词法语法解析器,而是直接通过正则匹配取出需要的内容,这里通过正则提取了:

  1. require() 里的 className

  2. defineClass() 里的 className

  3. defineClass() 里所有的方法名

  4. 文件里所有 keyword

同样每个 JS 文件都会生成一个 JPJSFile 对象,包含了上述提取的元素,并生成和保存了方法名和keyword对应的 completionItems列表。

组装和缓存

解决了单个 OC / JS 文件的解析,接着就是决定解析哪些文件,以什么样的形式缓存和组装返回给XCode。

JPObjcIndex

先看看 OC 的解析,JPObjcIndex 负责 OC 头文件的解析,JS 可能调用到的 OC 代码只存在于两个地方,一是系统framework,二是项目里的代码,对这些 OC 头文件要以什么样的方式解析呢?这里有两个选择:

  1. 只解析与当前编辑的 JS 文件相关的 OC 头文件

  2. 一次性把所有文件都解析好,再进行筛选

若要用方案1,只解析与当前编辑的 JS 文件相关的文件,则需要知道 JS 文件引用到了哪些 OC 文件,需要像 OC 代码那样有 #import 其他文件的规则,而 JSPatch 的规则是调用 OC 代码时不需要 import OC 文件,只需要通过 require(‘className') 接口引入类,所以线索只有 require() 里的类名,而在还没解析时是不清楚类名和 OC 文件的对应关系的,无法知道当前 JS 文件依赖了哪些 OC 头文件,所以这里只能选择一次性把所有 OC 头文件都解析好。

JPObjcIndex 默认扫描了 Foundation 和 UIKit 这两个 framework 里的所有头文件,以及当前项目里的所有 OC 头文件,在 JPObjcIndex 里以 className 为 key 进行缓存,对外提供通过 className 去取这些类相应 completionItem 的接口。

JPJSIndex

对于 JS 文件,为了简单起见,同样采用了一次性解析全部文件的方式。JPJSIndex 做了以下这些事:

  1. 解析所有 JS 文件,生成一个个对应的 JPJSFile 对象,缓存起来。

  2. 取出每个 JPJSFile 里解析的 require() 以及 defineClass() 的 className,去 JPObjcIndex 取这些 Class 对应的 completionItems。

  3. 取出每个 JPJSFile 解析好的 method completionItems。

  4. 取出每个 JPJSFile 解析好的 keyword completionItems。

  5. 本地 keyword.plist 定义了 JSPatch 常用的一些自动补全关键字,例如 defineClass, CGRect 等,在这里取出这些数据并生成 completionItems。

  6. 把 2-5 步里的 completionItems 分成两种类型,keyword 类型和 method 类型,缓存起来并返回给 XCode。

  7. 当有 JS 文件保存时,重新对这个文件生成 JPJSFile 对象,并重做 2-6 步。

第6步分出来的两种类型应用于两种场景,method 类型会在 JS 输入 . 要进行方法调用时出现,这个类型里所有的 completionItem 都是方法,包括 OC 头文件定义的方法以及 JS 里解析的方法。keyword 类型则是其他的像类名/语句关键字等这些非方法,在平常输入中出现。

在没有 JS 文件保存时,用户编辑 JS 代码每一次输入走到补全逻辑时,JPJSIndex 都是直接返回内存里已组装好的 completionItems 列表,没有其他操作,提高操作性能。第7步虽然在有文件保存时重新做了 2-6 步对数据进行重新组装,但这个过程不涉及文件解析,只需要取内存里解析好的数据进行组装,并且文件保存不会那么频繁,所以性能上没有太大问题。

整个流程就是这样,实际上很简单,总结起来就是解析所有 OC 头文件,解析所有 JS 文件,组装并缓存好 completionItem 返回。

不足

做这个项目的想法是先用最简单的方式快速做出来,满足80%的需求,导致会有一些不足,例如

  1. 没有做 JS 语法解析,没有做细致的筛选规则,粗暴地全部提示。

  2. 没有补全 include 的其他 JS 文件里的全局变量。

  3. defineClass 里写定义方法时,若要覆盖 OC 原有方法,没有方法名补全(因为方法名只有在 . 后才有补全)

  4. 没有加上除了 Foundation 和 UIKit 以外的 framework。(这可以通过在 JS 文件注释里显示引入需要的 framework)

欢迎一起改进 JSPatchX,完善这些不足~