endaaman.com

2020-05-15

Tips

NeoVimでC/C++を書くときはcoc.nvim + cclsが良さげ

時代はLSP!

NeoVim + coc.nvim + cclsという環境でなかなか快適にC/C++プロジェクトの開発を行えているので、そのセットアップなどを軽く書き残しておく。

coc.nvim

2020年前期の現在、NeoVim向けLSPクライアントのデファクトはneoclide/coc.nvimである。メンテナンスもよくされている。

Conquer of Completionの名は伊達ではなく、非常に高機能でよくできている。自分にとってNvimの補完に関してはこれ一本で一切不満がない(cocを導入を機にShougo/deoplete.nvimをやめてしまった)。

ccls

C/C++のLSPサーバーのデファクトは、MaskRay/cclsだろう。インストールはArch LinuxならAURにcclsがある。その他の人はリポジトリをcloneしてcmake --build Release --target installすればいいと思う(試してないから分からん)。

ただし、coc.nvimとcclsをインストールするだけでは、C/C++で補完やコードジャンプなどLSPの機能を使うことはできない。cocとcclsとそれぞれ自分で設定する必要がある。

coc.nvimの設定

cocのcclsに関する設定手順はLanguage servers · neoclide/coc.nvim Wikiにまとまっている。

NeoVimを起動して:CocConfigコマンドを実行すると、$XDG_CONFIG_HOME/nvim/coc-settings.jsonが開く。このJSONファイルがcocの設定ファイルとなっている。

{
  "languageserver": {
    "ccls": {
      "command": "ccls",
      "filetypes": ["c", "cpp", "cuda", "objc", "objcpp"],
      "rootPatterns": [".ccls-root", "compile_commands.json"],
      "initializationOptions": {
         "cache": {
           "directory": "/tmp/ccls-cache"
         }
       }
    }
  }
}

上記のように"languageserver"の下に"ccls"エントリを追加する。rootPatternsにマッチするファイルがあるディレクトリが、cclsのプロジェクトのルートとして識別される。"compile_commands.json"は後述するが、cclsで使用する.ctagsみたいなファイルである。自分はいらないと思ったが、適宜.gitなど追加するといい。

cclsの設定

cclsのセットアップ手順はProject Setup · MaskRay/ccls Wikiにまとまっている。cclsの設定と言うと、cclsのLSPサーバーに対して、シンボルの定義やその定義されたファイルパスなどを教えることと同義になるが、それにはいくつかの方法がある。そのうちもっとも簡便で一般的と思われる、compile_commands.jsonを使う方法を紹介する。

compile_commands.jsonを使う方法

そもそもcompile_commands.jsonとは、C/C++のコンパイルオプションを管理する方法のひとつである。中身は

[
    {
        "arguments": [
            "gcc",
            "-c",
            "-DHAVE_CONFIG_H",
            "-I.",
            "-I..",
            "-g3",
            "-O0",
            "-std=c11",
            "-Wall",
            "-Wextra",
            "-Wno-unused-parameter",
            "-Wno-sign-compare",
            "-Wno-pointer-sign",
            "-Wno-missing-field-initializers",
            "-Wformat=2",
            "-Wstrict-aliasing=2",
            "-Wdisabled-optimization",
            "-Wfloat-equal",
            "-Wpointer-arith",
            "-Wbad-function-cast",
            "-Wcast-align",
            "-Wredundant-decls",
            "-Winline",
            "-I../include",
            "-I/usr/include/gtk-3.0",
            "-I/usr/include/pango-1.0",
            "-I/usr/include/glib-2.0",
            "-I/usr/lib/glib-2.0/include",
            "-I/usr/include/harfbuzz",
            "-I/usr/include/freetype2",
            "-I/usr/include/libpng16",
            "-I/usr/include/fribidi",
            "-I/usr/include/cairo",
            "-I/usr/include/pixman-1",
            "-I/usr/include/gdk-pixbuf-2.0",
            "-I/usr/include/libmount",
            "-I/usr/include/blkid",
            "-I/usr/include/gio-unix-2.0",
            "-I/usr/include/atk-1.0",
            "-I/usr/include/at-spi2-atk/2.0",
            "-I/usr/include/dbus-1.0",
            "-I/usr/lib/dbus-1.0/include",
            "-I/usr/include/at-spi-2.0",
            "-I/usr/include/vte-2.91",
            "-pthread",
            "-o",
            "tym-meta.o",
            "meta.c"
        ],
        "directory": "/home/<$USER>/src/github.com/endaaman/tym/src",
        "file": "meta.c"
    },
    {
        "arguments": [
            "gcc",
            "-c",
            "-DHAVE_CONFIG_H",
            "-I.",
            "-I..",
            "-g3",
            "-O0",
            "-std=c11",
            "-Wall",
            "-Wextra",
            "-Wno-unused-parameter",
            "-Wno-sign-compare",
            "-Wno-pointer-sign",
            "-Wno-missing-field-initializers",
            "-Wformat=2",
            "-Wstrict-aliasing=2",
            "-Wdisabled-optimization",
            "-Wfloat-equal",
            "-Wpointer-arith",
            "-Wbad-function-cast",
            "-Wcast-align",
            "-Wredundant-decls",
            "-Winline",
            "-I../include",
            "-I/usr/include/gtk-3.0",
            "-I/usr/include/pango-1.0",
            "-I/usr/include/glib-2.0",
            "-I/usr/lib/glib-2.0/include",
            "-I/usr/include/harfbuzz",
            "-I/usr/include/freetype2",
            "-I/usr/include/libpng16",
            "-I/usr/include/fribidi",
            "-I/usr/include/cairo",
            "-I/usr/include/pixman-1",
            "-I/usr/include/gdk-pixbuf-2.0",
            "-I/usr/include/libmount",
            "-I/usr/include/blkid",
            "-I/usr/include/gio-unix-2.0",
            "-I/usr/include/atk-1.0",
            "-I/usr/include/at-spi2-atk/2.0",
            "-I/usr/include/dbus-1.0",
            "-I/usr/lib/dbus-1.0/include",
            "-I/usr/include/at-spi-2.0",
            "-I/usr/include/vte-2.91",
            "-pthread",
            "-o",
            "tym-app.o",
            "app.c"
        ],
        "directory": "/home/<$USER>/src/github.com/endaaman/tym/src",
        "file": "app.c"
    },    
]

というようなもので、一つのターゲットごとに"arguments""directory""file"の3つのプロパティを持つエントリの配列である。

しかしこれを用意すると言っても、こんなものを手で書く人間はいない。この世にはすでに、これを生成するためのツールが存在している。

bearを使ってcompile_commands.jsonを作る

rizsotto/Bearを使えば簡単にcompile_commands.jsonを作ることが出来る。Arch LinuxユーザーはAURにあるので簡単にインストールできる。その他の人はよく知らないが、一応

Bear is packaged for many distributions. Check out your package manager. Or build it from source.

<cite>rizsotto/Bear: Bear is a tool that generates a compilation database for clang tooling.</cite>

らしい。使い方は簡単で、

$ bear -- make

:::message "bearのコマンド引数について" 古いバージョンではbearにキャプチャさせるコマンドはただbearの後ろに $ bear make とするだけで良かったが、どこかのバージョンから $ bear -- make all というように -- を挟むようになった。ある「引数を含む完成されたコマンド」を文字列として引数に持つようなコマンドは(dockerやLXDのlxcなど)--が使用される。後続の引数が、引数側となっているコマンドのものか、メインで実行してるコマンドのものか明示的に区別するためのものである。 :::

と実行するだけである。仕組みは単純で、$ makeでコンパイルすると

make_output.png

こんな感じの出力があると思うが、このstdoutをキャプチャしてコンパイルオプションらしきものを集めてcompile_commands.jsonに書き出しているだけである。ちなみにmake以外にもいろいろ対応してるらしいが、自分は試してないのでどこまで面倒見てくれるか分からないので各自READMEを見に行ってほしい。

:::message "stdbool.hを#includeしてるのにundeclaredと怒られる問題"

stdbool.hを使用時に

bool_error.png

のような警告が出ることがある。これは、stdbool.h/usr/lib/clang/10.0.0/include/stdbool.hのようにclangと一緒に配布される形式になっていることに由来する。このようなclagのリソースの一部でもあるインクルードパス/usr/lib/clang/10.0.0/includeは、cclsがコンパイルされる際に、clang -print-resource-dirの結果を定数としてcclsに埋め込まれる。なので、cclsをビルドしたあとにclangのバージョンが上がったりするとcclsが参照するインクルードパスにはすでに存在しない状態に陥る可能性がある。ゆえに対処は簡単で、cclsをビルドし直すだけで直る。 :::

.cclsを使った共通ヘッダの追加

プロジェクトルートに.cclsという名前のファイルを置くと、そのファイルの各行をプロジェクト共通のコンパイルオプションとして追加することもできる。

たとえば.cclsファイルは

-I/path/to/include
%compile_commands.json

のようにすると、compile_commands.jsonを引き継ぎつつ、/path/to/includeを共通のインクルードパスに追加できる。%の他にもいろいろオプションがあるので、詳しくはGitHubリポジトリのwikiを確認してほしい。

あとは使うだけ

ここまでやれば不満なく動くはずである。よきLSPライフを。


©2024 endaaman.com