Random stuff I learned..

..mostly while coding

Configuring Emacs for C++

About every six months I think to myself: "I should configure Emacs to be a better C++ environment, with IDE-like features!". It invariably ends in tears and frustration, which takes roughly the aforementioned six months to forget.

Today was that day.

To make things a bit harder, I also decided to re-architect my init-file using use-package. I had some automatic package installation before, but configuration was a mess.

So, what to use for C++ development? I have some requirements:

I have tried cpputils-cmake before, but unfortunately it needs makefiles to parse the compiler flags. I have also tried cmake-ide, but I could never get it configured quite right, and it felt slow. I figured, maybe my new use-package based configuration is easier to get right? Let's give it a shot.

I made a minimal cmake-ide and company-mode configuration, something like this:

(use-package company
  :ensure t
  :init
  (global-company-mode))

(use-package cmake-ide
  :ensure t
  :init
  (cmake-ide-setup))

Hmm. It kind of works, but why does it create new cmake build folders all the time? Let's set cmake-ide-build-dir to something, using a .dir-locals.el file in the root of the project (replace PROJECT_ROOT with the proper path):

((nil . ((cmake-ide-build-dir . "<PROJECT_ROOT>/build"))))

Now it runs cmake in the proper place, and cmake-ide-compile seems to do the right thing. But now completion fails with a rather large error message. What's wrong? Oh, it seems to be missing system includes. Let's fix it:

(use-package cmake-ide
  :ensure t
  :init
  (use-package semantic/bovine/gcc)
  (setq cmake-ide-flags-c++ (append '("-std=c++11")
				    (mapcar (lambda (path) (concat "-I" path)) (semantic-gcc-get-include-paths "c++"))))
  (setq cmake-ide-flags-c (append (mapcar (lambda (path) (concat "-I" path)) (semantic-gcc-get-include-paths "c"))))
  (cmake-ide-setup))

Ok, now it works sometimes. But darn, it is slow as molasses. It has to compile the whole file for every completion with company-clang, and doesn't cache anything. :(

The tears and frustration are looming.

Let's try irony-mode instead. It's built on libclang, and runs a server process in the background. This means that emacs doesn't need to hang while waiting for completions. And it can keep state cached between queries.

(use-package irony
  :ensure t
  :config
  (use-package company-irony
    :ensure t
    :config
    (add-to-list 'company-backends 'company-irony))
  (use-package company-irony-c-headers
    :ensure t
    :config
    (add-to-list 'company-backends 'company-irony-c-headers))
  (add-hook 'c++-mode-hook 'irony-mode)
  (add-hook 'c-mode-hook 'irony-mode)
  (add-hook 'objc-mode-hook 'irony-mode)
  ;; replace the `completion-at-point' and `complete-symbol' bindings in
  ;; irony-mode's buffers by irony-mode's function
  (defun my-irony-mode-hook ()
    (define-key irony-mode-map [remap completion-at-point]
      'irony-completion-at-point-async)
    (define-key irony-mode-map [remap complete-symbol]
      'irony-completion-at-point-async))
  (add-hook 'irony-mode-hook 'my-irony-mode-hook)
  (add-hook 'irony-mode-hook 'irony-cdb-autosetup-compile-options))

I added the company-irony and company-irony-c-headers backends to company, and disabled company-clang:

(use-package company
  :ensure t
  :init
  (global-company-mode)
  :config
  (delete 'company-backends 'company-clang))

Note that you also need to install the server, which you can do with M-x irony-install-server. You need to have libclang-dev installed to build it.

Oh man, this works much better! The completion works, and is pretty quick too! There's still some slowness as the first file is opened, because cmake-ide needs to convert compile_commands.json produced by cmake to a format that irony likes. But it's not too bad.

Wee, no tears, no frustration!

Here's the whole shebang:

(use-package irony
  :ensure t
  :config
  (use-package company-irony
    :ensure t
    :config
    (add-to-list 'company-backends 'company-irony))
  (use-package company-irony-c-headers
    :ensure t
    :config
    (add-to-list 'company-backends 'company-irony-c-headers))
  (add-hook 'c++-mode-hook 'irony-mode)
  (add-hook 'c-mode-hook 'irony-mode)
  (add-hook 'objc-mode-hook 'irony-mode)
  ;; replace the `completion-at-point' and `complete-symbol' bindings in
  ;; irony-mode's buffers by irony-mode's function
  (defun my-irony-mode-hook ()
    (define-key irony-mode-map [remap completion-at-point]
      'irony-completion-at-point-async)
    (define-key irony-mode-map [remap complete-symbol]
      'irony-completion-at-point-async))
  (add-hook 'irony-mode-hook 'my-irony-mode-hook)
  (add-hook 'irony-mode-hook 'irony-cdb-autosetup-compile-options))

(use-package company
  :ensure t
  :init
  (global-company-mode)
  :bind (("<backtab>" . company-complete-common-or-cycle))
  :config
  (delete 'company-backends 'company-clang))

(use-package cmake-ide
  :ensure t
  :init
  (use-package semantic/bovine/gcc)
  (setq cmake-ide-flags-c++ (append '("-std=c++11")
				    (mapcar (lambda (path) (concat "-I" path)) (semantic-gcc-get-include-paths "c++"))))
  (setq cmake-ide-flags-c (append (mapcar (lambda (path) (concat "-I" path)) (semantic-gcc-get-include-paths "c"))))
  (cmake-ide-setup))

And one final tip. To avoid answering popups, and having to save safe local variables to your init file, add this to the cmake-ide config:

(put 'cmake-ide-build-dir 'safe-local-variable #'stringp)

Comments