开发者的实用 Vim 插件:自动补全神器–YouCompleteMe

第一次听说这个插件还是在偶然的情况下看到别人的博客,听说了这个插件的大名.本来打算在实训期间来完成安装的,无奈网实在不给力,也就拖到了回家的时候.在开始准备工作的时候就了解到这个插件不是很容易安装,安装的时候果然名不虚传.(关于这方面的内容,请查看另一篇文章)不过,有付出总有回报,安装之后用上这个插件,真心为这个插件的强大所折服.

那这个插件有何不同?

 

YouCompleteMe的特别之处

基于语义补全

总所周知,Vim是一款文本编辑器.也就是说,其最基础的工作就是编辑文本,而不管该文本的内容是什么.在Vim被程序员所使用后,其慢慢的被肩负了与IDE一样的工作,文本自动补全(ie.acp,omnicppcompleter),代码检查(Syntastic)等等工作.

针对文本自动补全这个功能来说,主要有两种实现方式.

  • 基于文本

我们常用的omnicppcompleter,acp,vim自带的c-x, c-n的实现方式就是基于文本.更通俗的说法,其实就是一个字:

其通过文本进行一些正则表达式的匹配,再根据生成的tags(利用ctags生成)来实现自动补全的效果.

  • 基于语义

顾名思义,其是通过分析源文件,经过语法分析以后进行补全.由于对源文件进行分析,基于语义的补全可以做到很精确.但是这显然是vim所不可能支持的.而且经过这么多年发展,由于语法分析有很高的难度,也一直没有合适的工具出现.直到,由apple支持的clang/llvm横空出世.YouCompleteMe也正是在clang/llvm的基础上进行构建的.

整合实现了多种插件

  • clang_complete
  • AutoComplPop
  • Supertab
  • neocomplcache
  • Syntastic(类似功能,仅仅针对c/c++/obj-c代码)

支持语言

  • c
  • c++
  • obj-c
  • c#
  • python

对于其他的语言,会调用vim设置的omnifunc来匹配,因此同样支持php,ruby等语言.

已知的有 * javascript —-tern_for_vim * ruby/java —-eclim

使用效果图

 

使用感受

  • 和IDE一样,自动补全,
  • 根据include的文件进行补全
  • 不用再蹩脚的生成tags
  • 补全非常精准,而且速度很快,不会有延迟(以前在大项目上,acp用起来实在是很卡)
  • 支持类似tags的跳转,跳到定义处以及使用处
  • 出错提示很智能,并且用起来真的是如丝般柔滑,不用输入:w进行强制检测

安装

说完了那么多好处,就要说到安装了.不同于以往其他vim插件,YCM是一款编译型的插件.在下载完后,需要手动编译后才能使用.对应其他的插件来说,仅仅就是把.vim的文件丢到相应文件夹下就可以.而这也加大了使用YCM的难度.

安装准备

  • 最新版的Vim(7.3.584+),编译时添加+python标志(已经安装的可以通过vim --version查看)
  • cmake(mac可以通过homebrew安装,brew install cmake,ubuntu可以通过sudo apt-get install cmake)
  • 安装vundle插件,用于安装管理vim的插件

mac下快速安装

.vimrc中添加下列代码

Bundle 'Valloric/YouCompleteMe'

保存退出后打开vim,在正常模式下输入

:BundleInstall

等待vundle将YouCompleteMe安装完成

而后进行编译安装:

cd ~/.vim/bundle/YouCompleteMe
./install --clang-completer

如果不需要c-family的补全,可以去掉--clang-completer.如果需要c#的补全,请加上--omnisharp-completer.

正常来说,YCM会去下载clang的包,如果已经有,也可以用系统--system-libclang.

就这样,安装结束.打开vim,如果没有提示YCM未编译,则说明安装已经成功了.

手动编译安装

安装的脚本并不是什么时候都好用,至少对我来说是这样的.安装完之后出现了问题,参考issue#809.

在用:BundleInstall安装完成或者使用

git clone --recursive https://github.com/Valloric/YouCompleteMe.git

获取最新的仓库,而后使用git submodule update --init --recursive确认仓库的完整性后,开始安装流程.

  1. 下载最新的clang二进制文件 YCM要求clang版本 > 3.2,一般来说都是下载最新的.
  2. 安装python-dev.(ubuntu下使用sudo apt-get install python-dev,mac下默认提供,否则请安装command line tools)
  3. 编译

    cd ~ 
    mkdir ycm_build 
    cd ycm_build 
    cmake -G "Unix Makefiles" -DPATH_TO_LLVM_ROOT=~/ycm_temp/llvm_root_dir . ~/.vim/bundle/YouCompleteMe/cpp 
    make ycm_support_libs

这里需要注意的是,~/ycm_temp/llvm_root_dir中包含的是根据第一步下载的压缩包解压出来的内容(包括include, bin等等文件)

这样就完成了,开始感受YCM提供的完全不逊色于大型IDE所提供的自动补全功能吧.

配置

不同于很多vim插件,YCM首先需要编译,另外还需要有配置.在vim启动后,YCM会找寻当前路径以及上层路径的.ycm_extra_conf.py.在~/.vim/bundle/YouCompleteMe/cpp/ycm/.ycm_extra_conf.py中提供了默认的模板.也可以参考我的(就在模板上改改而已).不过这个解决了标准库提示找不到的问题.

一般来说,我会在~目录下放一个默认的模板,而后再根据不同的项目在当前目录下再拷贝个.ycm_extra_conf.py.

# This file is NOT licensed under the GPLv3, which is the license for the rest
# of YouCompleteMe.
#
# Here's the license text for this file:
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>

import os
import ycm_core

# These are the compilation flags that will be used in case there's no
# compilation database set (by default, one is not set).
# CHANGE THIS LIST OF FLAGS. YES, THIS IS THE DROID YOU HAVE BEEN LOOKING FOR.
flags = [
'-Wall',
'-Wextra',
#'-Werror',
#'-Wc++98-compat',
'-Wno-long-long',
'-Wno-variadic-macros',
'-fexceptions',
'-stdlib=libc++',
# THIS IS IMPORTANT! Without a "-std=<something>" flag, clang won't know which
# language to use when compiling headers. So it will guess. Badly. So C++
# headers will be compiled as C headers. You don't want that so ALWAYS specify
# a "-std=<something>".
# For a C project, you would set this to something like 'c99' instead of
# 'c++11'.
'-std=c++11',
# ...and the same thing goes for the magic -x option which specifies the
# language that the files to be compiled are written in. This is mostly
# relevant for c++ headers.
# For a C project, you would set this to 'c' instead of 'c++'.
'-x',
'c++',
'-I',
'.',
'-isystem',
'/usr/include',
'-isystem',
'/usr/local/include',
'-isystem',
'/Library/Developer/CommandLineTools/usr/include',
'-isystem',
'/Library/Developer/CommandLineTools/usr/bin/../lib/c++/v1',
]


# Set this to the absolute path to the folder (NOT the file!) containing the
# compile_commands.json file to use that instead of 'flags'. See here for
# more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html
#
# Most projects will NOT need to set this to anything; you can just change the
# 'flags' list of compilation flags. Notice that YCM itself uses that approach.
compilation_database_folder = ''

if os.path.exists( compilation_database_folder ):
  database = ycm_core.CompilationDatabase( compilation_database_folder )
else:
  database = None

SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ]

def DirectoryOfThisScript():
  return os.path.dirname( os.path.abspath( __file__ ) )


def MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
  if not working_directory:
    return list( flags )
  new_flags = []
  make_next_absolute = False
  path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ]
  for flag in flags:
    new_flag = flag

    if make_next_absolute:
      make_next_absolute = False
      if not flag.startswith( '/' ):
        new_flag = os.path.join( working_directory, flag )

    for path_flag in path_flags:
      if flag == path_flag:
        make_next_absolute = True
        break

      if flag.startswith( path_flag ):
        path = flag[ len( path_flag ): ]
        new_flag = path_flag + os.path.join( working_directory, path )
        break

    if new_flag:
      new_flags.append( new_flag )
  return new_flags


def IsHeaderFile( filename ):
  extension = os.path.splitext( filename )[ 1 ]
  return extension in [ '.h', '.hxx', '.hpp', '.hh' ]


def GetCompilationInfoForFile( filename ):
  # The compilation_commands.json file generated by CMake does not have entries
  # for header files. So we do our best by asking the db for flags for a
  # corresponding source file, if any. If one exists, the flags for that file
  # should be good enough.
  if IsHeaderFile( filename ):
    basename = os.path.splitext( filename )[ 0 ]
    for extension in SOURCE_EXTENSIONS:
      replacement_file = basename + extension
      if os.path.exists( replacement_file ):
        compilation_info = database.GetCompilationInfoForFile(
          replacement_file )
        if compilation_info.compiler_flags_:
          return compilation_info
    return None
  return database.GetCompilationInfoForFile( filename )


def FlagsForFile( filename, **kwargs ):
  if database:
    # Bear in mind that compilation_info.compiler_flags_ does NOT return a
    # python list, but a "list-like" StringVec object
    compilation_info = GetCompilationInfoForFile( filename )
    if not compilation_info:
      return None

    final_flags = MakeRelativePathsInFlagsAbsolute(
      compilation_info.compiler_flags_,
      compilation_info.compiler_working_dir_ )

    # NOTE: This is just for YouCompleteMe; it's highly likely that your project
    # does NOT need to remove the stdlib flag. DO NOT USE THIS IN YOUR
    # ycm_extra_conf IF YOU'RE NOT 100% SURE YOU NEED IT.
    #try:
    #  final_flags.remove( '-stdlib=libc++' )
    #except ValueError:
    #  pass
  else:
    relative_to = DirectoryOfThisScript()
    final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to )

  return {
    'flags': final_flags,
    'do_cache': True
  }

YouCompleteMe提供的其他功能

YCM除了提供了基本的补全功能,自动提示错误的功能外,还提供了类似tags的功能:

  • 跳转到定义GoToDefinition
  • 跳转到声明GoToDeclaration
  • 以及两者的合体GoToDefinitionElseDeclaration

可以在.vimrc中配置相应的快捷键.

nnoremap <leader>gl :YcmCompleter GoToDeclaration<CR>
nnoremap <leader>gf :YcmCompleter GoToDefinition<CR>
nnoremap <leader>gg :YcmCompleter GoToDefinitionElseDeclaration<CR>

另外,YCM也提供了丰富的配置选项,同样在.vimrc中配置.具体请参考

let g:ycm_error_symbol = '>>'
let g:ycm_warning_symbol = '>*'

同时,YCM可以打开location-list来显示警告和错误的信息:YcmDiags.个人关于ycm的配置如下:

" for ycm
let g:ycm_error_symbol = '>>'
let g:ycm_warning_symbol = '>*'
nnoremap <leader>gl :YcmCompleter GoToDeclaration<CR>
nnoremap <leader>gf :YcmCompleter GoToDefinition<CR>
nnoremap <leader>gg :YcmCompleter GoToDefinitionElseDeclaration<CR>
nmap <F4> :YcmDiags<CR>

YCM提供的跳跃功能采用了vim的jumplist,往前跳和往后跳的快捷键为Ctrl+O以及Ctrl+I.

总结

YouCompleteMe是我用过的最爽的一个自动补全的插件了.之前使用acp时,遇到大文件基本上就卡死了,以至于都不怎么敢使用.由于YCM使用的时C/S结构,部分使用vim脚本编写,部分认为原生代码,使得跑起来速度飞快.

=============================================================================

 

官网参见: https://github.com/Valloric/YouCompleteMe#commands

虽然官网文档有一节说的是 "快速安装" , 但是作者说 "并不一定适用于你的环境" . 我并不认为我的环境上可以执行成功, 干脆就按照Full Installation Guide操作了.

第一步, 升级vim.

YCM要求vim版本为7.3以上, 而我的开发机的版本才是7.0. orz~~. 另外, 也不要指望0疼的内网yum源. 另外, 我也不想从源码编译vim(虽然YCM的作者说了一句"Don't worry, it's easy"~~). 于是开始在goooogle上找vim的rpm包. 那么问题来了, 一共需要几个rpm包呢? 在开发机上用这个命令: rpm -qa | grep vim, 发现有四个包:  vim-common, vim-enhanced, vim-minimal, vim-filesystem.

幸好公司里面可以直接上goooogle, 直接google "vim  rpm" 找到了这么一个网站: https://www.rpmfind.net/linux/rpm2html/search.php. 然后在这里面挨个去搜这几个包, 这个网站会把所有发行版的rpm包都列出来, 对于我的64位redhat6, 要选择el6_x86_64后缀的包. 将他们下载到pc上, 再rz命令传到开发机上, 安装之. 安装之前要先卸载掉之前的vim包. 然后就ok了, 的到了一个7.4版本的vim. (过程并不是轻描淡写的两句话就搞定了, 至少我把所谓的 "Unix哲学" fuck了一百遍, , , ,).

第二步, 下载YCM.

网上所有的攻略都说, 通过 Vundle 来下载YCM. 然而我这是内网机器啊啊啊啊, 并没法使用 Vundle . 我也尝试通过在pc上搭了个http代理, 然后让开发机通过pc的代理来访问外网. 然而github上的链接都是https~~~~~Orz, 日了狗了. 仔细观察了下YCM的目录结构, 发现也就是和一个普通的vim插件差不多嘛~~于是在pc上git clone, 然后打个zip包, 再rz到开发机上. 等等, 文档上有一句这样的话: If you don't install YCM with Vundle, make sure you have run  git submodule update --init --recursive after checking out the YCM repository (Vundle will do this for you) to fetch YCM's dependencies. 好吧, 先执行下git submodule update --init --recursive, 然后再打包传过去. 发现这个命令执行下去后, 多出来好多东西. 这些东西都是后面用的上的. 

将zip包解压到~/.vim/bundle/YouCompleteMe目录.

第三步, 安装clang.

YCM也是基于clang的补全. 安装clang肯定是必要的. 用类似于安装vim的方法, 在google上找clang的rpm包. 主要是clang-3.4.2-4.el6.x86_64.rpm, llvm-3.4.2-4.el6.x86_64.rpm, llvm-libs-3.4.2-4.el6.x86_64.rpm这三个包. 这里有一点很重要, 就是64位系统, 一定要装x86_64的包. 开始的时候装错了, clang装了i686的包(i686其实是32位的), 虽然clang也能正常用, 但是YCM编译C++引擎时就会杯具啦.

第四步, 安装CMake.

这次内网的yum源终于有了点作用了. 直接yum install cmake就安装成功了.

第五步, 编译C++引擎(ycm_support_lib).

如果不是用来补全C++, 那这一步就可以略过啦. 首先建立一个ycm_build目录(随便建在哪里都行. 这个只是一个临时目录而已). 然后cd ycm_build, 再执行

cmake -G "Unix Makefiles" -DUSE_SYSTEM_LIBCLANG=ON  .   ~/.vim/bundle/YouCompleteMe/third_party/ycmd/cpp

由于我使用的是系统的clang, 所以要用-DUSE_SYSTEM_LIBCLANG=ON.

这时候可以看到ycm_build目录下多出来一堆东西.

然后再执行 cmake --build . --target ycm_support_libs. 

这一步如果顺利, 那再好不过了.

第一次尝试的时候没有加-DUSE_SYSTEM_LIBCLANG=ON, 编译编译着就失败了.

第二次, 加了-DUSE_SYSTEM_LIBCLANG=ON之后, 执行到91%, 还是报错, 后来发现是clang安装了32位的版本, , , ,

终于编译通过啦, 这时候, 可以看到~/.vim/bundle/YouCompleteMe/third_party/ycmd目录下多了一个ycm_client_support.so和ycm_core.so, 这应该就是编译出来的C++引擎了.

第六步, 使YCM生效

YCM需要一个叫做.ycm_extra_conf.py 的文件作为YCM "入口" . 启动vim的时候会去寻找这个文件, 然后加载它(从当前目录逐层往上找). 这个文件主要的意义在于, 让clang能把当前的源码文件 "编译 " 通过. 因为YCM是基于语义补全的, 会对.cpp进行语法分析和语义分析. 于是就得告诉clang一些具体的编译参数(比较重要的是-I, 得让clang知道去哪些目录下找头文件). 如果clang不能正确的编译.cpp, 那么很多补全的功能就失效了. 通过 :YcmDiags 命令可以查看当前的文件有哪些编译错误. 这个文件有两个主要的部分: 一个是flags数组, 在这里面是填编译选项的. 一个是compilation_database_folder. 这个compilation_database_folder是依赖于clang的一个特性. clang可以在编译的时候导出一个数据库, 然后用这个compilation_database_folder加载这个clang的数据库就可以得到一个最精确最完整的补全和跳转. 由于我们的代码是gcc编译的(相信用clang编译的项目还是少数), 所以并没有数据库可以导出, 也只能用flags了. (这俩东西是二选一的. 如果有数据库, 优先读数据库, 没有的话就读flags).

参照模板修改了一下flags(主要就是加上几个 -I, 因为我们的代码中引用了很多公司内部的库的头文件, 得让clang找到这些头文件的位置).

官网上的指南, 到了上一步就没了. 然而这个时候YCM并没有生效呢~~~. 尝试把YouComplete目录下的autoload/youcompleteme.vim和plugin目录下的youcompleteme.vim均拷贝到~/.vim的autoload和plugin下, 这回终于有反应了, 但是报错说找不到ycm_core.so. 打开youcompleteme.vim, 发现里面在尝试从../third_party/ycmd和../python下找内容. 于是把~/.vim/bundle/YouCompleteMe/下的python和third_party目录都拷贝到~/.vim目录下.

这个时候, YCM终于可以用了. 写了一个test.cpp, 简单试了下, 不管是补全和跳转, 都很给力, 完全符合自己的预期. 另外YCM还有语法检查的功能, 但是我觉得错误标记太丑, 就给关了(let g:ycm_enable_diagnostic_signs = 0).

然后非常开心的用公司项目的源码试了下, , , 顿时脸一黑, , , UnicodeDecodeError: 'utf8' codec can't decode byte 0xcd in position 0: invalid continuation byte. 反复的报这个错误, YCM的各种功能完全都木有了. T_T顿时感觉辛辛苦苦二十年, 一夜回到解放前. 仔细分析了下, 难道是源码中的中文是gbk的格式导致的? 于是将一个cpp转成utf8格式, 再试下, 果然YCM又能用了. NM, 这怎么办, 难道要把公司的源码都搞成utf8的么? 这不科学. 果断先去goooogle一下, 发现已经有人遇到了这个问题: https://github.com/Valloric/YouCompleteMe/issues/1458 YCM的作者在另外一个帖子(https://github.com/Valloric/YouCompleteMe/issues/1378)中是这么回复的 "this might be related to whatever is set as your current file encoding in Vim". 看来猜测的不错, 毕竟作者是歪果仁, 凭啥让人家支持gbk嘞?

不过毕竟是开源项目, 最大的好处是可以看到源码, 并且可以去修改~~于是决定改改源码让YCM支持gbk. 首先利用这个命令 :YcmDebugInfo , 可以看到报错的详细信息:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/root/.vim/autoload/../python/ycm/youcompleteme.py", line 526, in DebugInfo
    'debug_info' )
  File "/root/.vim/autoload/../python/ycm/client/base_request.py", line 71, in PostDataToHandler
    timeout ) )
  File "/root/.vim/autoload/../python/ycm/client/base_request.py", line 166, in JsonFromFuture
    raise MakeServerException( response.json() )
ycmd.responses.ServerError: UnicodeDecodeError: 'utf8' codec can't decode byte 0xc1 in position 0: invalid start byte
E858: Eval did not return a valid python object

挨个文件打开看了看, 最后定位到问题可能出现在这里:

/root/.vim/third_party/ycmd/ycmd/utils.py, 中有一个RecursiveEncodeUnicodeToUtf8函数. 这个函数大概的意思是把源代码文件变成utf8格式, 然后后面再按utf8格式decode. 如果这一步有问题的话, 那么decode肯定会出错. 于是修改了这个函数成如下的样子:

def RecursiveEncodeUnicodeToUtf8( value ):
  if isinstance( value, unicode ):
    return value.encode( 'utf8' )
  if isinstance( value, str ):
    try:
        value = value.decode('GBK').encode('utf8')
    except UnicodeDecodeError,e:
        pass
    return value
  elif isinstance( value, collections.Mapping ):
    return dict( map( RecursiveEncodeUnicodeToUtf8, value.iteritems() ) ) 
  elif isinstance( value, collections.Iterable ):
    return type( value )( map( RecursiveEncodeUnicodeToUtf8, value ) ) 
  else:
    return value

主要是针对if isinstance( value, str ):分支的修改. 之前是直接return value了. 这就意味着返回了一个gbk格式的字符串, 放到后面按utf8解析肯定会错. 于是将这个值先按gbk decode, 再encode成utf8. 然后, 问题就解决啦!!

至此, YCM真的可以用啦!

效果确实完爆ctags几条街, 不枉我这么费力的折腾.

最后放上我的YCM配置:

let g:ycm_global_ycm_extra_conf = '/search/odin/code/.ycm_extra_conf.py'
let g:ycm_confirm_extra_conf = 0 
let g:ycm_key_invoke_completion='<C-i>'
set completeopt=longest,menu
autocmd InsertLeave * if pumvisible() == 0|pclose|endif
inoremap <expr> <CR> pumvisible() ? "\<C-y>" : "\<CR>"
let g:ycm_enable_diagnostic_signs = 0 
let g:ycm_enable_diagnostic_highlighting = 1 
let g:ycm_collect_identifiers_from_comments_and_strings = 0 
let g:ycm_complete_in_comments = 0 
let g:ycm_complete_in_strings = 0 
let g:ycm_min_num_of_chars_for_completion = 2

更多的选项可以参见官网上的 option 部分.

 

Vim YCM+Syntastic插件找不到头文件问题
    在.vimrc中设置g:syntastic_cpp_include_dirs,无效。

解决:
以kaldi工程为例,在.ycm_extra_conf.py文件的flags最后加入

'-isystem',
'/path/to/kaldi/src'

 

总结

google是在比baidu牛逼太多了. 没有google绝对玩不转.

仔细啃文档, 遇到的很多问题, 文档中都有提及.

转载于:https://my.oschina.net/chaenomeles/blog/824604

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值