一起看看VIM著名补全插件YouCompleteMe的架构和实现

1 背景

YouCompleteMe是vim上最著名的插件之一,对于长期使用Linux和vim进行服务端开发的技术人员来说或多或少都有耳闻。它的著名主要体现在两方面,一是它提供的语义补全又快又准,开发效率提升明显,vim8以后更是支持了异步IO的特性,YouCompleteMe的体验进一步得到了提升;二是它的编译安装过程极其容易出现各种问题,被冠上了“史上最难安装的vim插件”之名。简单例举下自己在安装过程使用中遇到的问题:

(1)编译安装新版vim8.1始终不支持python,后来发现没安装python-dev。

(2)llvm clang默认编译的是debug版,20GB的内存都会导致编译过程OOM。

(3)因为用了高版本libclang,其头文件与YouCompleteMe中引入的不同,导致补全和语法提示出现一些奇奇怪怪的问题,比如无法识别复杂类型的函数参数等。

(4)GBK字符集的文件导致补全出现问题,主要是ycmd抽取注释中的中文的时候,没有catch住这个UnicodeEncodeError异常。

……

既然已经在这上面花了那么多时间,过程中也有不少感悟和收获,索性就整理了一下YouCompleteMe的架构和实现,讲述一下它的工作原理。

2 版本信息

笔者的使用环境为64位centos7.2 + vim8.1.146 + YouCompleteMe(2018年7月最新版) + libclang6.0.1 + python 2.7.5 + cmake3.12.0 + gcc4.8.5。不排除随着插件更新,出现与文中说法不一致的可能性。

3 基本架构

YCM的基本架构就是一个基于HTTP+JSON通信的简单本地C/S模型,在启动vim的时候,同时会启动一个ycmd守护进程作为server端;而vim收集当前编辑的上下文信息,通过client发送请求到ycmd,ycmd中进行语义补全和语法分析后把结果返回给vim展示。

YouCompleteMe的基本架构

4 主要的流程分析

我们使用YCM最主要的两个功能就是语义补全和跳转,这里我们通过从代码级别讲述语义补全功能大致流程的方式来认识一下YCM的工作原理,跳转功能的流程也非常相似。此外,因为笔者主要是使用C++开发,所以下面的相关流程主要针对C++分支,其它语言其实类似。

下面分别从请求和回包两个过程介绍一下YCM的工作流程,两幅流程图给出了大致步骤,详细说明中则会深入探讨一下每一步的一些细节以及遇到的问题。

图中路径默认为YouCompleteMe插件的根目录。

4.1 请求过程

client端调用server的过程

详细说明:

1. 在vim中,用于输入一段代码,然后调用快捷键触发补全,最终会调用到InvokeCompletion/InvokeSemanticCompletion函数准备向ycmd请求进行语义补全。

2. ycm客户端收集当前触发补全的上下文环境,包括:本文件路径、触发补全的行号和列号、当前工作目录、编译参数、未保存的文件内容和vim buffer等,然后调用BuildRequestData方法组装请求包,异步发送请求给ycmd,接着客户端就进入poll等待回包的状态。这里的操作已经进入python中执行,另外异步发包也依赖于vim8以上的版本支持异步IO的特性。

3. 经过网络通信处理后,ycmd服务端在主入口ComputeCandidatesInner中,解析请求包中的内容,如果发现是要求补全头文件的话,则直接从flags中解析包含的头文件路径,查找头文件并返回;否则准备进一步调用libclang方法进行语义补全,部分代码如下:

4. 接上一步,发现不是补全头文件,则调用translation_unit_store_.GetOrCreate尝试创建或获取一个已有的translation_unit,避免重复创建以提高效率。这个translation_unit代表了一个编译单元,承担了直接和libclang打交道的任务,且这里开始由C++代码进行处理。YCM官方文档上对一个translation_unit的定义:

A translation unit consists of the file you are editing and all the files you are including with #include directives (directly or indirectly) in that file.

5. translation_unit调用libclang提供的clang_codeCompleteAt接口,把触发补全的上下文信息传递进去以进行补全。

6. libclang根据上下文实现了语义补全,至于如何实现的,就与YCM关系不大了,这里暂时没有再深入研究libclang源码。

4.2 回包过程

详细说明:

7. libclang处理后,返回语义补全结果到上层。

8. 调用ToCompletionDataVector进行结果适配,转换成方便vim展示的信息,即把libclang的返回结构CXCodeCompleteResults转换成了ycmd用的结构CompletionData,当中会做一些筛选和过滤,避免无用的信息和重复的补全字符串等。

9. 调用ConvertCompletionData和responses.BuildCompletionData方法构建匹配结果列表的请求回包,然后ycmd返回回包给ycm客户端。每一个匹配结果由以下这些元素构成:

insertion_text:补全时实际插入的字符文本

menu_text:补全时下拉菜单中完整显示的文本,比如补全函数时候会显示参数,补全变量时则只显示变量等

extra_menu_info:附加的下拉菜单信息,目前用于放返回值类型

kind:类型名称,如f代表函数,m代表程序变量等

detailed_info:详细信息,用于vim's preview window中展示补全的一些详细信息,如多个重载函数等

extra_data:附加信息,目前也是用于vim's preview window中展示注释信息

如图所示:

匹配结果在VIM中对应的元素

这些匹配结果信息就是我们使用ycm补全时看到的那个下拉框及其周围的信息。另外这里ycm源码中没有处理好非utf-8编码的问题,导致注释中如果出现GBK编码的中文则会抛出异常,导致整个ycmd不可用,修改后如下:

def ConvertCompletionData( completion_data ):                                   
  try:                                                                          
  # Chinese GAK char in comment will cause an UnicodeDecodeError, so catch it first
  # add by tyriqchen                                                            
    doc_string = completion_data.DocString()                                    
  except UnicodeDecodeError:                                                    
    doc_string = None                                                           
                                                                                                                         
  return responses.BuildCompletionData(                                         
    insertion_text = completion_data.TextToInsertInBuffer(),                    
    menu_text = completion_data.MainCompletionText(),                           
    extra_menu_info = completion_data.ExtraMenuInfo(),                          
    kind = completion_data.kind_.name,                                          
    detailed_info = completion_data.DetailedInfoForPreviewWindow(),             
    extra_data = ( { 'doc_string': doc_string }                                 
                   if doc_string else None ) )                                  
    #extra_data = ( { 'doc_string': completion_data.DocString() }               
#               if completion_data.DocString() else None ) )

这里初步探究是因为libclang返回的结果字符串存在一些问题,一旦调用completion_data.DocString()方法直接就会抛出异常,甚至无法查看其内容或做转码,笔者对python不是很熟,欢迎高手指教是否有更好的解决方法。

10. ycm客户端接收来自ycmd端的回包,做协议转换后回传到上一层

11. 在第1步中 vim调用InvokeCompletion/InvokeSemanticCompletion以后,内部会调用PollCompletion方法异步等待补全结果回包;而PollCompletion内部则设置timer,定时去poll等待回包,最终接收补全结果回包后,调用Complete方法进行语义补全,把匹配结果列表展示给用户,整个补全流程结束。

上面讲的两个流程抽象化了一些东西,更多细节可以到源码里对应的地方去琢磨。整个链路经过了vim调用python再调用c++代码,然后再原路返回的一个过程,这里也从侧面说明了为什么安装YCM时相关的依赖又多又难装的原因。

4.3 其它实现要点

ycmd内有一个重要的模块叫completer,顾名思义就是补全器,用于提供补全功能,位于YouCompleteMe/third_party/ycmd/ycmd/completers目录下,里面有针对各种不同语言的completer。用户可以可以继承Completer基类,实现其中ComputeCandidatesInner等方法,定义一个自制的completer,有兴趣的读者可以继续深入研究下去。

Completer及其子类

5 总结

抛开网络通信部分不谈,YCM插件的client端的实现很简单,大部分的逻辑处理工作其实都放在了ycmd(a code-completion & comprehension server)这个server端。ycmd提供了代码补全,语义理解(即支持跳转)和针对一些语言实现了语法检查,同时为了支持多种语言,ycmd内部必然就有针对不同语言的Completer的实现,这在一定程度上提高了ycmd的复杂性。但从本质上来说,ycmd自身做的工作并不复杂,从上面的两个流程图可以看出,都是一条直线处理的逻辑,其实就是接受请求,协议转换,调用libclang,回包而已,最困难的语法解析和语义补全已经都交给libclang去做了。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏瓜大三哥

Matlab基本语法8

程序调试和编程技巧 在编写matlab程序时,难免会出现错误,这时就需要对程序进行调试。matlab中,m文件调试主要有两种方法:直接调试法和工具调试法。 拼写...

23270
来自专栏Golang语言社区

goroutine背后的系统知识

Go语言从诞生到普及已经三年了,先行者大都是Web开发的背景,也有了一些普及型的书籍,可系统开发背景的人在学习这些书籍的时候,总有语焉不详的感觉,网上也有若干流...

35340
来自专栏王小雷

Python之pandas数据加载、存储

Python之pandas数据加载、存储 0. 输入与输出大致可分为三类: 0.1 读取文本文件和其他更好效的磁盘存储格式 2.2 使用数据库中的数据 0.3 ...

25670
来自专栏ml

web基础之Structs(一篇)

为什么有 struts 框架 Struct 的优点之处: 1.       struct的好处 2.       程序更加规范化 3.       程序的可...

39780
来自专栏后台全栈之路

基于汇编的 C/C++ 协程 - 实现

将 libco 和 libevent 两者的功能糅合起来,所以我把我的工程,命名为 libcoevent,意为 “基于 libevent 的同步协程服务器编程框...

60330
来自专栏塔奇克马敲代码

第 8 章 IO库

20250
来自专栏张善友的专栏

.net 2.0 你是如何使用事务处理?

     事务处理作为企业级开发必备的基础设施, .net 2.0通过System.Transactions对事务提供强大的支持.你还是在使用.net 1.x下...

22560
来自专栏和蔼的张星的图像处理专栏

1.Win10+VsCode的C/CPP编译环境搭建

我是从开始学C++的时候就一直用的是visual studio,毕竟宇宙第一IDE,写和调试都是超级方便快捷,唯一的缺点可能就是启动慢一点。 之前电脑没有换固...

85860
来自专栏Golang语言社区

Goroutine背后的系统知识

Go语言从诞生到普及已经三年了,先行者大都是Web开发的背景,也有了一些普及型的书籍,可系统开发背景的人在学习这些书籍的时候,总有语焉不详的感觉,网上也有若干流...

35560
来自专栏代码世界

Python之进程

进程 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机...

68470

扫码关注云+社区

领取腾讯云代金券