EmacsのC/C++開発環境を整える [lsp-mode, ccls]

EmacsのLSPクライアントとして現在盛んに開発されているlsp-modeと、C/C++/Objective-CのLSPサーバ(言語サーバ)であるcclsを用いて、C/C++の開発環境を整えてみました。

lsp-mode + ccls demo
lsp-mode + cclsでC++を書いている様子

私がよく利用しているUbuntu16.04環境(そろそろ18.04にアップグレードします…)へ導入した話なので、他の環境に導入したい方は参考程度にご覧ください。

1. はじめに

今までC/C++のプロジェクトではironyrtagsを利用していたのですが、最近lsp-modeの存在を知り、試しに導入してみたところ、これがかなり快適でした。

冒頭にも書きましたが、lsp-modeとはLSPクライアントとして動作するEmacsのパッケージです。

これに関しては以下の@Ladicleさんの記事がとても参考になります。

私個人の意見ですが、lsp-modeを採用することには以下の利点があると考えています。

  1. 従来のパッケージに比べてSyntax check, 定義ジャンプ, 参照検索等が軽量に動作する
  2. 言語サーバに備わっている豊富な機能を利用することが出来る
  3. 様々な言語に適用することが可能で、キーバインド等のUIを統一することが出来る

1つ目に関して、これは、構文解析がEmacsとは別のプロセスで動作する言語サーバ上で行われるため、従来のパッケージと比べて軽量に動作するということです。とはいえ、例えばironyはlibclangを利用して構文解析を行うirony-serverという専用アプリケーションを別プロセスで起動させますし、従来のパッケージでも物によっては同様に軽量であるといえます。

2つ目に関して、これは、従来のEmacsの構文解析系のパッケージで実現しきれていなかった、LSPで定義されている様々な機能を利用出来るということです。言語サーバによってはLSPで定義されている機能の一部しかサポートしていなかったりするのですが、それでも出来ることは従来より多いかと思われます。

3つ目ですが、個人的にこれが一番嬉しいところです。私はC/C++の他に、PHPなどを書くことがあるのですが、今までは言語ごとに別々のパッケージをインストールして設定を行っていました。別々のパッケージを利用するということは、言語ごとにコマンドやコマンドに割り当てるキーバインドが異なるという訳で、色々と(精神的に消耗することが多く)大変です。ここで、lsp-modeを採用することによって、各言語の言語サーバを事前にインストールしておくだけで、lsp-modeという共通のインターフェースを通じて定義ジャンプ等のコマンドを実行出来るようになります。

2. 言語サーバの用意

前置きが長くなってしまいましたが、C/C++/Objective-Cの言語サーバであるcclsをインストールしていきます。

cclsのビルド・インストールの方法に関しては、リポジトリのWikiに書かれていますので、この通りに進めていきます。

追記 2020-04-20

Ubuntu18.04へのcclsのインストールは以下の記事を参照下さい。

2.1 CMakeのインストール

Ubuntu16.04のパッケージマネージャでCMakeをインストールすると、CMake 3.5.1がインストールされます。しかし残念ながら、cclsをビルドするのに必要なCMakeのバージョンは3.8以上です。

とりあえず最新のCMakeをインストールしておきます。

$ git clone -b release --depth=1 https://github.com/Kitware/CMake.git
$ cd CMake
$ ./bootstrap && make && sudo make install

この状態でCMakeコマンドを入力しても、パッケージマネージャでインストールされたCMake(/usr/bin/cmake)が呼ばれてしまいます。適当にaliasでも張っておきましょう。

$ echo "alias cmake=/usr/local/bin/cmake" << ~/.bashrc
$ source ~/.bashrc
$ cmake --version
cmake version 3.14.3
 
CMake suite maintained and supported by Kitware (kitware.com/cmake).

(このブログを書いている時点で)最新のCMake 3.14.3がインストールされました。

2.2 GCC-7のインストール

Ubuntu16.04のパッケージマネージャでインストールされるGCCのバージョンは(以下略)

GCC7.2以上が要求されるようなので、これをインストールします。

$ sudo add-apt-repository ppa:ubuntu-toolchain-r/test
$ sudo apt update
$ sudo apt install gcc-7 g++-7
$ gcc-7 --version
gcc-7 (Ubuntu 7.4.0-1ubuntu1~16.04~ppa1) 7.4.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

2.3 LLVM/Clangのインストール

最新のLLVM/Clangをインストールしておきます。

LLVMのビルドは結構時間がかかるので注意してください。

# 最新のLLVM/Clangをインストールする場合
$ sudo apt install subversion
$ svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
$ cd llvm/tools
$ svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
$ cd clang/tools
$ svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra
$ cd ../../../projects
$ svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt
$ cd ../..
$ mkdir llvm.build
$ cd llvm.build
$ cmake -G "Unix Makefiles" \
        -DCMAKE_BUILD_TYPE=Release \
        -DCMAKE_INSTALL_PREFIX=/usr/local ../llvm
$ make # 時間がかかるのでccacheの使用, job数の指定をおすすめします
$ sudo make install

追記 2020-06-08

ビルド済みのバイナリが配布されているのでそちらを使用した方が簡単です。ここで、ダウンロードした LLVM/Clang のビルド済みのバイナリは削除しないように注意してください。

2.4 cclsのインストール

やっとのことでcclsをインストールすることが出来ます。

$ cd /usr/src
$ sudo git clone --depth=1 --recursive https://github.com/MaskRay/ccls
$ cd ccls
$ sudo wget -c https://releases.llvm.org/8.0.0/clang+llvm-8.0.0-x86_64-linux-gnu-ubuntu-16.04.tar.xz
$ sudo tar xf clang+llvm-8.0.0-x86_64-linux-gnu-ubuntu-16.04.tar.xz
$ sudo cmake -H. \
             -BRelease \
             -DCMAKE_BUILD_TYPE=Release \
             -DCMAKE_CXX_COMPILER=g++-7 \
             -DCMAKE_PREFIX_PATH=$PWD/clang+llvm-8.0.0-x86_64-linux-gnu-ubuntu-16.04
$ sudo cmake --build Release --target install

これでcclsが/usr/local/bin/下にインストールされます。

3. init.elの記述

無事にcclsがインストール出来たのでEmacsの設定の方に移ります。

私の記述の一部を紹介しますが、ここではuse-packageを使用しています。使用していない方は申し訳ないのですが適宜読み替えてください。

3.1 lsp-mode

  1. (use-package lsp-mode
  2.   :commands lsp
  3.   :custom
  4.   ((lsp-enable-snippet t)
  5.    (lsp-enable-indentation nil)
  6.    (lsp-prefer-flymake nil)
  7.    (lsp-document-sync-method 2)
  8.    (lsp-inhibit-message t)
  9.    (lsp-message-project-root-warning t)
  10.    (create-lockfiles nil))
  11.   :init
  12.   (unbind-key "C-l")
  13.   :bind
  14.   (("C-l C-l"  . lsp)
  15.    ("C-l h"    . lsp-describe-session)
  16.    ("C-l t"    . lsp-goto-type-definition)
  17.    ("C-l r"    . lsp-rename)
  18.    ("C-l <f5>" . lsp-restart-workspace)
  19.    ("C-l l"    . lsp-lens-mode))
  20.   :hook
  21.   (prog-major-mode . lsp-prog-major-mode-enable))

lsp-modeには、lsp-format-bufferというフォーマッタを使ってコードの自動整形を行うコマンドが用意されているのですが、言語サーバにcclsを利用している際にこれを実行すると、ソースコードがめちゃくちゃになる場合があります…(lsp-modeのバグなのかcclsのバグなのかは不明)。

5行目のようにlsp-enable-indentationnilを指定しておくと、lsp-format-bufferが実行不能になり、暴発を防ぐことが出来ます。

3.2 lsp-ui

  1. (use-package lsp-ui
  2.   :commands lsp-ui-mode
  3.   :after lsp-mode
  4.   :custom
  5.   ;; lsp-ui-doc
  6.   (lsp-ui-doc-enable t)
  7.   (lsp-ui-doc-header t)
  8.   (lsp-ui-doc-include-signature t)
  9.   (lsp-ui-doc-position 'top)
  10.   (lsp-ui-doc-max-width  60)
  11.   (lsp-ui-doc-max-height 20)
  12.   (lsp-ui-doc-use-childframe t)
  13.   (lsp-ui-doc-use-webkit nil)
  14.  
  15.   ;; lsp-ui-flycheck
  16.   (lsp-ui-flycheck-enable t)
  17.  
  18.   ;; lsp-ui-sideline
  19.   (lsp-ui-sideline-enable t)
  20.   (lsp-ui-sideline-ignore-duplicate t)
  21.   (lsp-ui-sideline-show-symbol t)
  22.   (lsp-ui-sideline-show-hover t)
  23.   (lsp-ui-sideline-show-diagnostics t)
  24.   (lsp-ui-sideline-show-code-actions t)
  25.  
  26.   ;; lsp-ui-imenu
  27.   (lsp-ui-imenu-enable nil)
  28.   (lsp-ui-imenu-kind-position 'top)
  29.  
  30.   ;; lsp-ui-peek
  31.   (lsp-ui-peek-enable t)
  32.   (lsp-ui-peek-always-show t)
  33.   (lsp-ui-peek-peek-height 30)
  34.   (lsp-ui-peek-list-width 30)
  35.   (lsp-ui-peek-fontify 'always)
  36.   :hook
  37.   (lsp-mode . lsp-ui-mode)
  38.   :bind
  39.   (("C-l s"   . lsp-ui-sideline-mode)
  40.    ("C-l C-d" . lsp-ui-peek-find-definitions)
  41.    ("C-l C-r" . lsp-ui-peek-find-references)))

lsp-mode専用のUIを提供するlsp-uiの設定です。

詳細は公式を参照してください。

3.3 company & company-lsp

  1. (use-package company
  2.   :custom
  3.   (company-transformers '(company-sort-by-backend-importance))
  4.   (company-idle-delay 0)
  5.   (company-echo-delay 0)
  6.   (company-minimum-prefix-length 2)
  7.   (company-selection-wrap-around t)
  8.   (completion-ignore-case t)
  9.   :bind
  10.   (("C-M-c" . company-complete))
  11.   (:map company-active-map
  12.         ("C-n" . company-select-next)
  13.         ("C-p" . company-select-previous)
  14.         ("C-s" . company-filter-candidates)
  15.         ("C-i" . company-complete-selection)
  16.         ([tab] . company-complete-selection))
  17.   (:map company-search-map
  18.         ("C-n" . company-select-next)
  19.         ("C-p" . company-select-previous))
  20.   :init
  21.   (global-company-mode t)
  22.   :config
  23.   ;; lowercaseを優先にするソート
  24.   (defun my-sort-uppercase (candidates)
  25.     (let (case-fold-search
  26.           (re "\\`[[:upper:]]*\\'"))
  27.       (sort candidates
  28.             (lambda (s1 s2)
  29.               (and (string-match-p re s2)
  30.                    (not (string-match-p re s1)))))))
  31.  
  32.   (push 'my-sort-uppercase company-transformers)
  33.  
  34.   ;; yasnippetとの連携
  35.   (defvar company-mode/enable-yas t)
  36.   (defun company-mode/backend-with-yas (backend)
  37.     (if (or (not company-mode/enable-yas) (and (listp backend) (member 'company-yasnippet backend)))
  38.         backend
  39.       (append (if (consp backend) backend (list backend))
  40.               '(:with company-yasnippet))))
  41.   (setq company-backends (mapcar #'company-mode/backend-with-yas company-backends)))
  42.  
  43. (use-package company-lsp
  44.   :commands company-lsp
  45.   :custom
  46.   (company-lsp-cache-candidates nil)
  47.   (company-lsp-async t)
  48.   (company-lsp-enable-recompletion t)
  49.   (company-lsp-enable-snippet t)
  50.   :after
  51.   (:all lsp-mode lsp-ui company yasnippet)
  52.   :init
  53.   (push 'company-lsp company-backends))

companyはEmacsの補完インターフェースを提供するパッケージです。

company-backendsというリストに独自の補完エンジンを追加することで、補完機能の拡張を行うことが出来ます。lsp-modeとcompanyの連携を行うには、このリストにcompany-lspを追加します。

スニペット機能を提供するyasnippetも合わせて使用することをおすすめします。

  1. (use-package yasnippet
  2.   :bind
  3.   (:map yas-minor-mode-map
  4.         ("C-x i n" . yas-new-snippet)
  5.         ("C-x i v" . yas-visit-snippet-file)
  6.         ("C-M-i"   . yas-insert-snippet))
  7.   (:map yas-keymap
  8.         ("<tab>" . nil)) ;; because of avoiding conflict with company keymap
  9.   :init
  10.   (yas-global-mode t))

追記 2020-06-28

company-lsp は現在非推奨になっています。

Code completion – company-capf / completion-at-point (note that company-lsp is no longer supported).

lsp-mode/README.md at 97f91274f1174274c1b6e68c1edb35e8008895f5 · emacs-lsp/lsp-mode · GitHub

代わりに company-capf を使用することが推奨されているので、 company-lsp を disabled にして lsp-mode の設定に以下を追記してください。

:custom (lsp-prefer-capf t)

詳しくは以下の記事をご覧ください。

3.4 ccls

  1. (use-package ccls
  2.   :custom
  3.   (ccls-executable "/usr/local/bin/ccls")
  4.   (ccls-sem-highlight-method 'font-lock)
  5.   (ccls-use-default-rainbow-sem-highlight)
  6.   :hook ((c-mode c++-mode objc-mode) .
  7.          (lambda () (require 'ccls) (lsp))))

ccls-executableにインストールしたcclsのパスを指定します。

4. おわりに

導入は大変ですが、快適なのは間違いないので、今の時代でもEmacsを使うよという方には是非おすすめしたいと思います。

ところで、cclsはcompile_commands.jsonを読んでそのプロジェクトに沿った構文解析を行ってくれます。このjsonファイルは、CMakeを使うプロジェクトの場合CMAKE_EXPORT_COMPILE_COMMANDSを有効にすると出力されますので、CMakeLists.txtに追記しておくことをおすすめします(Makefileから出力する方法もあります)。ironyのようにbuildディレクトリ下のものを読んでくれると嬉しいのですが、プロジェクトルートにないとダメなようなのでシンボリックリンクを張ってあげましょう…。

追記 2020-06-09

Bazelを用いたプロジェクトでcompile_commands.jsonを出力する方法を以下の記事に書きました。

EmacsのC/C++開発環境を整える [lsp-mode, ccls]」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です