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

私がよく利用しているUbuntu16.04環境(そろそろ18.04にアップグレードします…)へ導入した話なので、他の環境に導入したい方は参考程度にご覧ください。
1. はじめに
今までC/C++のプロジェクトではironyやrtagsを利用していたのですが、最近lsp-modeの存在を知り、試しに導入してみたところ、これがかなり快適でした。
冒頭にも書きましたが、lsp-modeとはLSPクライアントとして動作するEmacsのパッケージです。
これに関しては以下の@Ladicleさんの記事がとても参考になります。
私個人の意見ですが、lsp-modeを採用することには以下の利点があると考えています。
- 従来のパッケージに比べてSyntax check, 定義ジャンプ, 参照検索等が軽量に動作する
- 言語サーバに備わっている豊富な機能を利用することが出来る
- 様々な言語に適用することが可能で、キーバインド等の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
(use-package lsp-mode
:commands lsp
:custom
((lsp-enable-snippet t)
(lsp-enable-indentation nil)
(lsp-prefer-flymake nil)
(lsp-document-sync-method 2)
(lsp-inhibit-message t)
(lsp-message-project-root-warning t)
(create-lockfiles nil))
:init
(unbind-key "C-l")
:bind
(("C-l C-l" . lsp)
("C-l h" . lsp-describe-session)
("C-l t" . lsp-goto-type-definition)
("C-l r" . lsp-rename)
("C-l <f5>" . lsp-restart-workspace)
("C-l l" . lsp-lens-mode))
:hook
(prog-major-mode . lsp-prog-major-mode-enable))
lsp-modeには、lsp-format-bufferというフォーマッタを使ってコードの自動整形を行うコマンドが用意されているのですが、言語サーバにcclsを利用している際にこれを実行すると、ソースコードがめちゃくちゃになる場合があります…(lsp-modeのバグなのかcclsのバグなのかは不明)。
5行目のようにlsp-enable-indentationにnilを指定しておくと、lsp-format-bufferが実行不能になり、暴発を防ぐことが出来ます。
3.2 lsp-ui
(use-package lsp-ui
:commands lsp-ui-mode
:after lsp-mode
:custom
;; lsp-ui-doc
(lsp-ui-doc-enable t)
(lsp-ui-doc-header t)
(lsp-ui-doc-include-signature t)
(lsp-ui-doc-position 'top)
(lsp-ui-doc-max-width 60)
(lsp-ui-doc-max-height 20)
(lsp-ui-doc-use-childframe t)
(lsp-ui-doc-use-webkit nil)
;; lsp-ui-flycheck
(lsp-ui-flycheck-enable t)
;; lsp-ui-sideline
(lsp-ui-sideline-enable t)
(lsp-ui-sideline-ignore-duplicate t)
(lsp-ui-sideline-show-symbol t)
(lsp-ui-sideline-show-hover t)
(lsp-ui-sideline-show-diagnostics t)
(lsp-ui-sideline-show-code-actions t)
;; lsp-ui-imenu
(lsp-ui-imenu-enable nil)
(lsp-ui-imenu-kind-position 'top)
;; lsp-ui-peek
(lsp-ui-peek-enable t)
(lsp-ui-peek-always-show t)
(lsp-ui-peek-peek-height 30)
(lsp-ui-peek-list-width 30)
(lsp-ui-peek-fontify 'always)
:hook
(lsp-mode . lsp-ui-mode)
:bind
(("C-l s" . lsp-ui-sideline-mode)
("C-l C-d" . lsp-ui-peek-find-definitions)
("C-l C-r" . lsp-ui-peek-find-references)))
lsp-mode専用のUIを提供するlsp-uiの設定です。
詳細は公式を参照してください。
3.3 company & company-lsp
(use-package company
:custom
(company-transformers '(company-sort-by-backend-importance))
(company-idle-delay 0)
(company-echo-delay 0)
(company-minimum-prefix-length 2)
(company-selection-wrap-around t)
(completion-ignore-case t)
:bind
(("C-M-c" . company-complete))
(:map company-active-map
("C-n" . company-select-next)
("C-p" . company-select-previous)
("C-s" . company-filter-candidates)
("C-i" . company-complete-selection)
([tab] . company-complete-selection))
(:map company-search-map
("C-n" . company-select-next)
("C-p" . company-select-previous))
:init
(global-company-mode t)
:config
;; lowercaseを優先にするソート
(defun my-sort-uppercase (candidates)
(let (case-fold-search
(re "\\`[[:upper:]]*\\'"))
(sort candidates
(lambda (s1 s2)
(and (string-match-p re s2)
(not (string-match-p re s1)))))))
(push 'my-sort-uppercase company-transformers)
;; yasnippetとの連携
(defvar company-mode/enable-yas t)
(defun company-mode/backend-with-yas (backend)
(if (or (not company-mode/enable-yas) (and (listp backend) (member 'company-yasnippet backend)))
backend
(append (if (consp backend) backend (list backend))
'(:with company-yasnippet))))
(setq company-backends (mapcar #'company-mode/backend-with-yas company-backends)))
(use-package company-lsp
:commands company-lsp
:custom
(company-lsp-cache-candidates nil)
(company-lsp-async t)
(company-lsp-enable-recompletion t)
(company-lsp-enable-snippet t)
:after
(:all lsp-mode lsp-ui company yasnippet)
:init
(push 'company-lsp company-backends))
companyはEmacsの補完インターフェースを提供するパッケージです。
company-backendsというリストに独自の補完エンジンを追加することで、補完機能の拡張を行うことが出来ます。lsp-modeとcompanyの連携を行うには、このリストにcompany-lspを追加します。
スニペット機能を提供するyasnippetも合わせて使用することをおすすめします。
(use-package yasnippet
:bind
(:map yas-minor-mode-map
("C-x i n" . yas-new-snippet)
("C-x i v" . yas-visit-snippet-file)
("C-M-i" . yas-insert-snippet))
(:map yas-keymap
("<tab>" . nil)) ;; because of avoiding conflict with company keymap
:init
(yas-global-mode t))
追記 2020-06-28
company-lsp は現在非推奨になっています。
Code completion –
lsp-mode/README.md at 97f91274f1174274c1b6e68c1edb35e8008895f5 · emacs-lsp/lsp-mode · GitHubcompany-capf
/completion-at-point
(note that company-lsp is no longer supported).
代わりに company-capf を使用することが推奨されているので、 company-lsp を disabled にして lsp-mode の設定に以下を追記してください。
:custom (lsp-prefer-capf t)
詳しくは以下の記事をご覧ください。
3.4 ccls
(use-package ccls
:custom
(ccls-executable "/usr/local/bin/ccls")
(ccls-sem-highlight-method 'font-lock)
(ccls-use-default-rainbow-sem-highlight)
:hook ((c-mode c++-mode objc-mode) .
(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件のフィードバック