CygwinでYouCompleteMeを無理矢理つかう

YouCompleteMeはvim用の補完プラグインで、基本的にCygwinはサポートしていない。まぁでも手でビルドすれば(動く動かないという意味で言えば)動くので使っている。

ycm_core.dllのビルド

YouCompleteMeは、

と、ycmdと呼ばれる

  • Pythonで書かれた一部言語向けの補完コード
  • C/C++で書かれたlibclangインターフェース

を内包したHTTPサーバで構成されていて、C/C++で書かれたネイティブ部分はインストール時にビルドする必要がある。Cygwin上でlibclangを使用したプログラムをリンクするには微妙なコツが有り

EXTRA_CMAKE_ARGS="-DCMAKE_CXX_FLAGS=-Wa,-mbig-obj" ./build.py --system-libclang --clang-completer --enable-debug

のようにしてリポジトリ( https://github.com/Valloric/ycmd )上のbuild.pyを起動する。このとき、Cygwin側にはlibclang等が予め導入されている必要がある(最近のCygwinではディストリビューションに含まれるため、setup.exeから普通にインストールできる)。
mbig-objはCOFFの制約を回避するのにどうしても必要になる。このオプションが無いと、

/home/oku/repos2/ycmd/cpp/ycm/ycm_core.cpp
/usr/lib/gcc/x86_64-pc-cygwin/5.4.0/../../../../x86_64-pc-cygwin/bin/as:
ycm/CMakeFiles/ycm_core.dir/ycm_core.cpp.o: too many sections (36370)

のようなエラーが出てリンクに失敗する。
標準のbuild.pyはPythonのライブラリをちゃんと検出しないため、

のようにパッチする必要がある。
ビルドするとリポジトリのルートに ycm_core.dll が作成されるので、これをプラグイン本体( https://github.com/Valloric/YouCompleteMe )のチェックアウトの third_party/ycmd/ycm_core.dll にsymlinkする。
手順を纏めると、

  1. cygwinにlibclang-develやpython-devel等必要そうなライブラリを適当に導入する
  2. リポジトリ https://github.com/Valloric/YouCompleteMehttps://github.com/Valloric/ycmd をチェックアウトし、submoduleを git submodule update --init --recursive で初期化する
  3. ycmdをビルドする
    1. https://github.com/okuoku/ycmd/commit/81be9b8e349564195d9ce98d82eb6cdf3127d52c のようにパッチ
    2. EXTRA_CMAKE_ARGS を与えてbuild.pyを起動
  4. YouCompleteMe/ycmd/ycm_core.dll のsymlinkを作る
  5. VimプラグインとしてYouCompleteMeのチェックアウトを追加する

個人的にはvimプラグインvim-plug( https://github.com/junegunn/vim-plug )で入れているので、

call plug#begin('~/.vim/plugged/')
Plug '~/repos/YouCompleteMe', {'on':[]}
call plug#end()

のようにして指定している。後ろの'on'指定はデフォルトでは初期化しない指定(後述)。

特定のディレクトリを編集するときにだけYouCompleteMeを起動する

↑のように、vim-plugで導入したプラグインは有効化条件を空にすることで.vimrcなりなんなりで有効化条件を自由に設定できる。
別にどういう方法を使っても良い気がするけど、個人的にはvim-rooter( https://github.com/airblade/vim-rooter )を使って

  • .vimrc
function! UseYcm()
  call plug#load('YouCompleteMe')
endfunction

"" Project definitions
if filereadable(expand('~/.projects.vim'))
  source ~/.projects.vim
endif

" Project dispatch
function! MyProjectInit()
  if !exists("g:myProjects")
    return
  endif
  let s:myProjectPaths = keys(g:myProjects)
  let s:myRootDir = FindRootDirectory()

  for e in s:myProjectPaths
    let pth = expand(e)
    if pth !=# s:myRootDir
      continue
    endif
    call UseYcm()
  endfor
endfunction

au VimEnter
        \ *.c,*.cc,*.cxx,*.cpp
        \ call MyProjectInit()
  • .projects.vim (手書き)
let g:myProjects = {
            \ '~/repos/yuni' : 'native' }

FindRootDirectory()はvim-rooterが提供する関数で、編集するファイルが事前に定義したプロジェクトに含まれるかどうかを判定するのに使っている。vim-plugはload手続きを使うことで任意のプラグインを後からロードさせることができる。

CMakeビルドディレクトリのcompile_commands.jsonへの対応

nmoshにせよteslawireにせよ、個人的なプロジェクトは全てCMakeでビルド環境を作っていて、ソースコードとビルドディレクトリは分割している。今のところYouCompleteMeはこのような分割を良くサポートしていないので、手でスクリプトを書くことでどうにかする必要がある。
YouCompleteMeでは、FindFlagsForFile手続きをディレクトリ毎に提供することでこれを行わせるのが通常の方法となるが、流石に面倒なのでグローバルなスクリプトで対応することを考える。

  • .vim/my_ycm_conf.py
import os
import ycm_core

class cmakeProject:
    def __init__(self, projdir, builddir):
        self.absProjDir = os.path.abspath(os.path.expanduser(projdir))
        self.absBuildDir = os.path.abspath(os.path.expanduser(builddir))
        self.db = False

    def doGetFlags(self, fn):
        if not self.db:
            self.db = ycm_core.CompilationDatabase(self.absBuildDir)
        if self.db:
            ifo = self.db.GetCompilationInfoForFile(fn)
            if not ifo:
                return False
            return ifo.compiler_flags_

        else:
            return False

    def getFlags(self, fn):
        apath = os.path.abspath(fn)
        cmn0 = os.path.commonprefix([apath, self.absProjDir])
        cmn = os.path.commonprefix([cmn0, self.absProjDir])
        if cmn == self.absProjDir:
            return self.doGetFlags(fn)
        else:
            return False

projects = [
        cmakeProject("~/repos/yuni", "~/yuni")
        ]


defaultflags = [
        '-Wall',
        '-isystem', '/usr/include',
        '-isystem', '/usr/local/include',
        '-isystem', '/usr/include/w32api'
        ]

def FlagsForFile(fn, **args):
    flgs = defaultflags
    # FIXME: Implement header file handling...
    for prj in projects:
        f = prj.getFlags(fn)
        if f:
            flgs = f
            break
    return { 'flags' : flgs }

このような内容の .vim/my_ycm_conf.py を作り、.vimrc側で

let g:ycm_confirm_extra_conf = 0
let g:ycm_global_ycm_extra_conf = '~/.vim/my_ycm_conf.py'

のように設定する。これで、事前にcmakeProjectとして設定したディレクトリのファイルを編集する際は、ビルドディレクトリ側のcompile_commands.jsonを読ませることができる。

ToDo

Androidのプロジェクトをどう補完させるか。。Cygwinに限らずYouCompleteMeのような補完機構は大体クロス開発への考察がなく、上手いこと統合できない。
YouCompleteMeの場合は、ycmdを起動するためのpythonインタプリタのパスを設定できるため、適当なproxyサーバとの組み合せでどうにかできないだろうか。